Squashed commit of the following:

commit b87b6b2a96de870f1782b00cfe3f393cc79b7d3b
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Mon Dec 18 14:05:11 2017 +0000

    Even more tests for this micropub refactor

commit 2d967f33c3abeea8fc89f91e1764e970681dc58f
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Sat Dec 16 22:19:53 2017 +0000

    Fill out token endpoint tests

commit 440dcbe3e53f058060c918429bea75911ddafdc1
Merge: 02a25b0 f60164f
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Fri Dec 15 14:32:50 2017 +0000

    Merge pull request #77 from jonnybarnes/analysis-8KABW6

    Apply fixes from StyleCI

commit f60164fe81dbcc1d2343704145d26c6d6412579a
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Fri Dec 15 14:27:40 2017 +0000

    Apply fixes from StyleCI

commit 02a25b083a0305f73d715feb3f9d34f9de8f67d4
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Fri Dec 15 14:17:43 2017 +0000

    phpcs fix

commit 144998de0866bf11f235847d7edc076235294545
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Fri Dec 15 14:16:59 2017 +0000

    Don’t pass the request object to service files, pass request()->all()

commit dd5e52010c51a359665efa349ff8c13d4d6dbf57
Merge: 97b270a 23b145e
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Sun Dec 10 20:01:03 2017 +0000

    Merge pull request #76 from jonnybarnes/analysis-86AVg6

    Apply fixes from StyleCI

commit 23b145e7bf67a358b3cb894ea0793984b65ecab5
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Sun Dec 10 19:50:53 2017 +0000

    Apply fixes from StyleCI

commit 97b270a89abe92e167e0d363029ae0b86608bbc9
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Sun Dec 10 19:43:38 2017 +0000

    Improve test coverage of the refactor

commit 244102264559e4fb0b0614d1738c0283703a71dc
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Fri Dec 8 13:31:13 2017 +0000

    Refactor the note creation code

commit 22b4786cbd7ae508b51a47f0c8cf9a15535edbb1
Author: Jonny Barnes <jonny@jonnybarnes.uk>
Date:   Thu Dec 7 17:39:41 2017 +0000

    Remove ununsed importsed classes in the controller
This commit is contained in:
Jonny Barnes 2017-12-18 15:51:02 +00:00
parent d409098efb
commit 43beae2f82
15 changed files with 875 additions and 286 deletions

View file

@ -2,41 +2,35 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Storage;
use Monolog\Logger; use Monolog\Logger;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use App\Jobs\ProcessMedia; use App\Jobs\ProcessMedia;
use App\Services\TokenService;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use App\{Like, Media, Note, Place}; use App\{Like, Media, Note, Place};
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\{Request, Response}; use Illuminate\Http\{Request, Response};
use App\Exceptions\InvalidTokenException; use App\Exceptions\InvalidTokenException;
use Phaza\LaravelPostgis\Geometries\Point; use Phaza\LaravelPostgis\Geometries\Point;
use Intervention\Image\Exception\NotReadableException; use Intervention\Image\Exception\NotReadableException;
use App\Services\{NoteService, PlaceService, TokenService};
use App\Services\Micropub\{HCardService, HEntryService, UpdateService}; use App\Services\Micropub\{HCardService, HEntryService, UpdateService};
class MicropubController extends Controller class MicropubController extends Controller
{ {
protected $tokenService; protected $tokenService;
protected $noteService;
protected $placeService;
protected $hentryService; protected $hentryService;
protected $hcardService; protected $hcardService;
protected $updateService; protected $updateService;
public function __construct( public function __construct(
TokenService $tokenService, TokenService $tokenService,
NoteService $noteService,
PlaceService $placeService,
HEntryService $hentryService, HEntryService $hentryService,
HCardService $hcardService, HCardService $hcardService,
UpdateService $updateService UpdateService $updateService
) { ) {
$this->tokenService = $tokenService; $this->tokenService = $tokenService;
$this->noteService = $noteService;
$this->placeService = $placeService;
$this->hentryService = $hentryService; $this->hentryService = $hentryService;
$this->hcardService = $hcardService; $this->hcardService = $hcardService;
$this->updateService = $updateService; $this->updateService = $updateService;
@ -46,13 +40,12 @@ class MicropubController extends Controller
* This function receives an API request, verifies the authenticity * This function receives an API request, verifies the authenticity
* then passes over the info to the relavent Service class. * then passes over the info to the relavent Service class.
* *
* @param \Illuminate\Http\Request request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function post(Request $request) public function post()
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->bearerToken()); $tokenData = $this->tokenService->validateToken(request()->bearerToken());
} catch (InvalidTokenException $e) { } catch (InvalidTokenException $e) {
return $this->invalidTokenResponse(); return $this->invalidTokenResponse();
} }
@ -61,13 +54,13 @@ class MicropubController extends Controller
return $this->tokenHasNoScopeResponse(); return $this->tokenHasNoScopeResponse();
} }
$this->logMicropubRequest($request); $this->logMicropubRequest(request()->all());
if (($request->input('h') == 'entry') || ($request->input('type.0') == 'h-entry')) { if ((request()->input('h') == 'entry') || (request()->input('type.0') == 'h-entry')) {
if (stristr($tokenData->getClaim('scope'), 'create') === false) { if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->insufficientScopeResponse(); return $this->insufficientScopeResponse();
} }
$location = $this->hentryService->process($request); $location = $this->hentryService->process(request()->all(), $this->getCLientId());
return response()->json([ return response()->json([
'response' => 'created', 'response' => 'created',
@ -75,11 +68,11 @@ class MicropubController extends Controller
], 201)->header('Location', $location); ], 201)->header('Location', $location);
} }
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') { if (request()->input('h') == 'card' || request()->input('type')[0] == 'h-card') {
if (stristr($tokenData->getClaim('scope'), 'create') === false) { if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->insufficientScopeResponse(); return $this->insufficientScopeResponse();
} }
$location = $this->hcardService->process($request); $location = $this->hcardService->process(request()->all());
return response()->json([ return response()->json([
'response' => 'created', 'response' => 'created',
@ -87,13 +80,18 @@ class MicropubController extends Controller
], 201)->header('Location', $location); ], 201)->header('Location', $location);
} }
if ($request->input('action') == 'update') { if (request()->input('action') == 'update') {
if (stristr($tokenData->getClaim('scope'), 'update') === false) { if (stristr($tokenData->getClaim('scope'), 'update') === false) {
return $this->returnInsufficientScopeResponse(); return $this->insufficientScopeResponse();
} }
return $this->updateService->process($request); return $this->updateService->process(request()->all());
} }
return response()->json([
'response' => 'error',
'error_description' => 'unsupported_request_type',
], 500);
} }
/** /**
@ -102,34 +100,33 @@ class MicropubController extends Controller
* appropriately. Further if the request has the query parameter * appropriately. Further if the request has the query parameter
* synidicate-to we respond with the known syndication endpoints. * synidicate-to we respond with the known syndication endpoints.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function get(Request $request) public function get()
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->bearerToken()); $tokenData = $this->tokenService->validateToken(request()->bearerToken());
} catch (InvalidTokenException $e) { } catch (InvalidTokenException $e) {
return $this->invalidTokenResponse(); return $this->invalidTokenResponse();
} }
if ($request->input('q') === 'syndicate-to') { if (request()->input('q') === 'syndicate-to') {
return response()->json([ return response()->json([
'syndicate-to' => config('syndication.targets'), 'syndicate-to' => config('syndication.targets'),
]); ]);
} }
if ($request->input('q') == 'config') { if (request()->input('q') == 'config') {
return response()->json([ return response()->json([
'syndicate-to' => config('syndication.targets'), 'syndicate-to' => config('syndication.targets'),
'media-endpoint' => route('media-endpoint'), 'media-endpoint' => route('media-endpoint'),
]); ]);
} }
if (substr($request->input('q'), 0, 4) === 'geo:') { if (substr(request()->input('q'), 0, 4) === 'geo:') {
preg_match_all( preg_match_all(
'/([0-9\.\-]+)/', '/([0-9\.\-]+)/',
$request->input('q'), request()->input('q'),
$matches $matches
); );
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000; $distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
@ -155,13 +152,12 @@ class MicropubController extends Controller
/** /**
* Process a media item posted to the media endpoint. * Process a media item posted to the media endpoint.
* *
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\Response * @return Illuminate\Http\Response
*/ */
public function media(Request $request) public function media()
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->bearerToken()); $tokenData = $this->tokenService->validateToken(request()->bearerToken());
} catch (InvalidTokenException $e) { } catch (InvalidTokenException $e) {
return $this->invalidTokenResponse(); return $this->invalidTokenResponse();
} }
@ -174,7 +170,7 @@ class MicropubController extends Controller
return $this->insufficientScopeResponse(); return $this->insufficientScopeResponse();
} }
if (($request->hasFile('file') && $request->file('file')->isValid()) === false) { if ((request()->hasFile('file') && request()->file('file')->isValid()) === false) {
return response()->json([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'invalid_request', 'error' => 'invalid_request',
@ -182,13 +178,13 @@ class MicropubController extends Controller
], 400); ], 400);
} }
$this->logMicropubRequest($request); $this->logMicropubRequest(request()->all());
$filename = $this->saveFile($request->file('file')); $filename = $this->saveFile(request()->file('file'));
$manager = resolve(ImageManager::class); $manager = resolve(ImageManager::class);
try { try {
$image = $manager->make($request->file('file')); $image = $manager->make(request()->file('file'));
$width = $image->width(); $width = $image->width();
} catch (NotReadableException $exception) { } catch (NotReadableException $exception) {
// not an image // not an image
@ -196,9 +192,9 @@ class MicropubController extends Controller
} }
$media = Media::create([ $media = Media::create([
'token' => $request->bearerToken(), 'token' => request()->bearerToken(),
'path' => 'media/' . $filename, 'path' => 'media/' . $filename,
'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()), 'type' => $this->getFileTypeFromMimeType(request()->file('file')->getMimeType()),
'image_widths' => $width, 'image_widths' => $width,
]); ]);
@ -234,6 +230,7 @@ class MicropubController extends Controller
$videoMimeTypes = [ $videoMimeTypes = [
'video/mp4', 'video/mp4',
'video/mpeg', 'video/mpeg',
'video/ogg',
'video/quicktime', 'video/quicktime',
'video/webm', 'video/webm',
]; ];
@ -254,18 +251,24 @@ class MicropubController extends Controller
return 'download'; return 'download';
} }
private function logMicropubRequest(Request $request) private function getClientId(): string
{
return resolve(TokenService::class)
->validateToken(request()->bearerToken())
->getClaim('client_id');
}
private function logMicropubRequest(array $request)
{ {
$logger = new Logger('micropub'); $logger = new Logger('micropub');
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG); $logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG);
$logger->debug('MicropubLog', $request->all()); $logger->debug('MicropubLog', $request);
} }
private function saveFile(UploadedFile $file) private function saveFile(UploadedFile $file)
{ {
$filename = Uuid::uuid4() . '.' . $file->extension(); $filename = Uuid::uuid4() . '.' . $file->extension();
$size = $file->getClientSize(); Storage::disk('local')->put($filename, $file);
Storage::disk('local')->put($filename, $file->openFile()->fread($size));
return $filename; return $filename;
} }

View file

@ -136,6 +136,9 @@ class Place extends Model
public function setExternalUrlsAttribute($url) public function setExternalUrlsAttribute($url)
{ {
if ($url === null) {
return;
}
$type = $this->getType($url); $type = $this->getType($url);
$already = []; $already = [];
if (array_key_exists('external_urls', $this->attributes)) { if (array_key_exists('external_urls', $this->attributes)) {

View file

@ -21,25 +21,27 @@ class BookmarkService
/** /**
* Create a new Bookmark. * Create a new Bookmark.
* *
* @param Request $request * @param array $request
* @return Bookmark $bookmark
*/ */
public function createBookmark(Request $request): Bookmark public function createBookmark(array $request): Bookmark
{ {
if ($request->header('Content-Type') == 'application/json') { if (array_get($request, 'properties.bookmark-of.0')) {
//micropub request //micropub request
$url = normalize_url($request->input('properties.bookmark-of.0')); $url = normalize_url(array_get($request, 'properties.bookmark-of.0'));
$name = $request->input('properties.name.0'); $name = array_get($request, 'properties.name.0');
$content = $request->input('properties.content.0'); $content = array_get($request, 'properties.content.0');
$categories = $request->input('properties.category'); $categories = array_get($request, 'properties.category');
} }
if (($request->header('Content-Type') == 'application/x-www-form-urlencoded') if (array_get($request, 'bookmark-of')) {
|| $url = normalize_url(array_get($request, 'bookmark-of'));
(str_contains($request->header('Content-Type'), 'multipart/form-data')) $name = array_get($request, 'name');
) { $content = array_get($request, 'content');
$url = normalize_url($request->input('bookmark-of')); $categories = array_get($request, 'category');
$name = $request->input('name'); }
$content = $request->input('content');
$categories = $request->input('category'); if (! isset($url)) {
throw new \Exception;
} }
$bookmark = Bookmark::create([ $bookmark = Bookmark::create([
@ -55,11 +57,11 @@ class BookmarkService
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name'); $targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
$mpSyndicateTo = null; $mpSyndicateTo = null;
if ($request->has('mp-syndicate-to')) { if (array_get($request, 'mp-syndicate-to')) {
$mpSyndicateTo = $request->input('mp-syndicate-to'); $mpSyndicateTo = array_get($request, 'mp-syndicate-to');
} }
if ($request->has('properties.mp-syndicate-to')) { if (array_get($request, 'properties.mp-syndicate-to')) {
$mpSyndicateTo = $request->input('properties.mp-syndicate-to'); $mpSyndicateTo = array_get($request, 'properties.mp-syndicate-to');
} }
if (is_string($mpSyndicateTo)) { if (is_string($mpSyndicateTo)) {
$service = array_search($mpSyndicateTo, $targets); $service = array_search($mpSyndicateTo, $targets);

View file

@ -6,26 +6,27 @@ namespace App\Services;
use App\Like; use App\Like;
use App\Jobs\ProcessLike; use App\Jobs\ProcessLike;
use Illuminate\Http\Request;
class LikeService class LikeService
{ {
/** /**
* Create a new Like. * Create a new Like.
* *
* @param Request $request * @param array $request
* @return Like $like
*/ */
public function createLike(Request $request): Like public function createLike(array $request): Like
{ {
if ($request->header('Content-Type') == 'application/json') { if (array_get($request, 'properties.like-of.0')) {
//micropub request //micropub request
$url = normalize_url($request->input('properties.like-of.0')); $url = normalize_url(array_get($request, 'properties.like-of.0'));
} }
if (($request->header('Content-Type') == 'application/x-www-form-urlencoded') if (array_get($request, 'like-of')) {
|| $url = normalize_url(array_get($request, 'like-of'));
($request->header('Content-Type') == 'multipart/form-data') }
) {
$url = normalize_url($request->input('like-of')); if (! isset($url)) {
throw new \Exception();
} }
$like = Like::create(['url' => $url]); $like = Like::create(['url' => $url]);

View file

@ -2,30 +2,23 @@
namespace App\Services\Micropub; namespace App\Services\Micropub;
use Illuminate\Http\Request;
use App\Services\PlaceService; use App\Services\PlaceService;
class HCardService class HCardService
{ {
public function process(Request $request) public function process(array $request)
{ {
$data = []; $data = [];
if ($request->header('Content-Type') == 'application/json') { if (array_get($request, 'properties.name')) {
$data['name'] = $request->input('properties.name'); $data['name'] = array_get($request, 'properties.name');
$data['description'] = $request->input('properties.description') ?? null; $data['description'] = array_get($request, 'properties.description');
if ($request->has('properties.geo')) { $data['geo'] = array_get($request, 'properties.geo');
$data['geo'] = $request->input('properties.geo');
}
} else { } else {
$data['name'] = $request->input('name'); $data['name'] = array_get($request, 'name');
$data['description'] = $request->input('description'); $data['description'] = array_get($request, 'description');
if ($request->has('geo')) { $data['geo'] = array_get($request, 'geo');
$data['geo'] = $request->input('geo'); $data['latitude'] = array_get($request, 'latitude');
} $data['longitude'] = array_get($request, 'longitude');
if ($request->has('latitude')) {
$data['latitude'] = $request->input('latitude');
$data['longitude'] = $request->input('longitude');
}
} }
$place = resolve(PlaceService::class)->createPlace($data); $place = resolve(PlaceService::class)->createPlace($data);

View file

@ -2,26 +2,25 @@
namespace App\Services\Micropub; namespace App\Services\Micropub;
use Illuminate\Http\Request;
use App\Services\{BookmarkService, LikeService, NoteService}; use App\Services\{BookmarkService, LikeService, NoteService};
class HEntryService class HEntryService
{ {
public function process(Request $request) public function process(array $request, string $client = null)
{ {
if ($request->has('properties.like-of') || $request->has('like-of')) { if (array_get($request, 'properties.like-of') || array_get($request, 'like-of')) {
$like = resolve(LikeService::class)->createLike($request); $like = resolve(LikeService::class)->createLike($request);
return $like->longurl; return $like->longurl;
} }
if ($request->has('properties.bookmark-of') || $request->has('bookmark-of')) { if (array_get($request, 'properties.bookmark-of') || array_get($request, 'bookmark-of')) {
$bookmark = resolve(BookmarkService::class)->createBookmark($request); $bookmark = resolve(BookmarkService::class)->createBookmark($request);
return $bookmark->longurl; return $bookmark->longurl;
} }
$note = resolve(NoteService::class)->createNote($request); $note = resolve(NoteService::class)->createNote($request, $client);
return $note->longurl; return $note->longurl;
} }

View file

@ -4,14 +4,13 @@ namespace App\Services\Micropub;
use App\Note; use App\Note;
use App\Media; use App\Media;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
class UpdateService class UpdateService
{ {
public function process(Request $request) public function process(array $request)
{ {
$urlPath = parse_url($request->input('url'), PHP_URL_PATH); $urlPath = parse_url(array_get($request, 'url'), PHP_URL_PATH);
//is it a note we are updating? //is it a note we are updating?
if (mb_substr($urlPath, 1, 5) !== 'notes') { if (mb_substr($urlPath, 1, 5) !== 'notes') {
@ -31,8 +30,8 @@ class UpdateService
} }
//got the note, are we dealing with a “replace” request? //got the note, are we dealing with a “replace” request?
if ($request->has('replace')) { if (array_get($request, 'replace')) {
foreach ($request->input('replace') as $property => $value) { foreach (array_get($request, 'replace') as $property => $value) {
if ($property == 'content') { if ($property == 'content') {
$note->note = $value[0]; $note->note = $value[0];
} }
@ -58,8 +57,8 @@ class UpdateService
} }
//how about “add” //how about “add”
if ($request->has('add')) { if (array_get($request, 'add')) {
foreach ($request->input('add') as $property => $value) { foreach (array_get($request, 'add') as $property => $value) {
if ($property == 'syndication') { if ($property == 'syndication') {
foreach ($value as $syndicationURL) { foreach ($value as $syndicationURL) {
if (starts_with($syndicationURL, 'https://www.facebook.com')) { if (starts_with($syndicationURL, 'https://www.facebook.com')) {
@ -75,7 +74,7 @@ class UpdateService
} }
if ($property == 'photo') { if ($property == 'photo') {
foreach ($value as $photoURL) { foreach ($value as $photoURL) {
if (start_with($photo, 'https://')) { if (starts_with($photoURL, 'https://')) {
$media = new Media(); $media = new Media();
$media->path = $photoURL; $media->path = $photoURL;
$media->type = 'image'; $media->type = 'image';
@ -91,5 +90,10 @@ class UpdateService
'response' => 'updated', 'response' => 'updated',
]); ]);
} }
return response()->json([
'response' => 'error',
'error_description' => 'unsupported request',
], 500);
} }
} }

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Services; namespace App\Services;
use Illuminate\Http\Request;
use App\{Media, Note, Place}; use App\{Media, Note, Place};
use App\Jobs\{SendWebMentions, SyndicateNoteToFacebook, SyndicateNoteToTwitter}; use App\Jobs\{SendWebMentions, SyndicateNoteToFacebook, SyndicateNoteToTwitter};
@ -13,190 +12,38 @@ class NoteService
/** /**
* Create a new note. * Create a new note.
* *
* @param \Illuminate\Http\Request $request * @param array $request
* @param string $client
* @return \App\Note $note * @return \App\Note $note
*/ */
public function createNote(Request $request): Note public function createNote(array $request, string $client = null): Note
{ {
//move the request to data code here before refactor
$data = [];
$data['client-id'] = resolve(TokenService::class)
->validateToken($request->bearerToken())
->getClaim('client_id');
if ($request->header('Content-Type') == 'application/json') {
if (is_string($request->input('properties.content.0'))) {
$data['content'] = $request->input('properties.content.0'); //plaintext content
}
if (is_array($request->input('properties.content.0'))
&& array_key_exists('html', $request->input('properties.content.0'))
) {
$data['content'] = $request->input('properties.content.0.html');
}
$data['in-reply-to'] = $request->input('properties.in-reply-to.0');
// check location is geo: string
if (is_string($request->input('properties.location.0'))) {
$data['location'] = $request->input('properties.location.0');
}
// check location is h-card
if (is_array($request->input('properties.location.0'))) {
if ($request->input('properties.location.0.type.0' === 'h-card')) {
try {
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
$request->input('properties.location.0')
);
$data['checkin'] = $place->longurl;
} catch (\Exception $e) {
//
}
}
}
$data['published'] = $request->input('properties.published.0');
//create checkin place
if (array_key_exists('checkin', $request->input('properties'))) {
$data['swarm-url'] = $request->input('properties.syndication.0');
try {
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
$request->input('properties.checkin.0')
);
$data['checkin'] = $place->longurl;
} catch (\Exception $e) {
$data['checkin'] = null;
$data['swarm-url'] = null;
}
}
} else {
$data['content'] = $request->input('content');
$data['in-reply-to'] = $request->input('in-reply-to');
$data['location'] = $request->input('location');
$data['published'] = $request->input('published');
}
$data['syndicate'] = [];
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
$mpSyndicateTo = null;
if ($request->has('mp-syndicate-to')) {
$mpSyndicateTo = $request->input('mp-syndicate-to');
}
if ($request->has('properties.mp-syndicate-to')) {
$mpSyndicateTo = $request->input('properties.mp-syndicate-to');
}
if (is_string($mpSyndicateTo)) {
$service = array_search($mpSyndicateTo, $targets);
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
if ($service == 'Facebook') {
$data['syndicate'][] = 'facebook';
}
}
if (is_array($mpSyndicateTo)) {
foreach ($mpSyndicateTo as $uid) {
$service = array_search($uid, $targets);
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
if ($service == 'Facebook') {
$data['syndicate'][] = 'facebook';
}
}
}
$data['photo'] = [];
$photos = null;
if ($request->has('photo')) {
$photos = $request->input('photo');
}
if ($request->has('properties.photo')) {
$photos = $request->input('properties.photo');
}
if ($photos !== null) {
foreach ($photos as $photo) {
if (is_string($photo)) {
//only supporting media URLs for now
$data['photo'][] = $photo;
}
}
if (starts_with($request->input('properties.syndication.0'), 'https://www.instagram.com')) {
$data['instagram-url'] = $request->input('properties.syndication.0');
}
}
//check the input
if (array_key_exists('content', $data) === false) {
$data['content'] = null;
}
if (array_key_exists('in-reply-to', $data) === false) {
$data['in-reply-to'] = null;
}
if (array_key_exists('client-id', $data) === false) {
$data['client-id'] = null;
}
$note = Note::create( $note = Note::create(
[ [
'note' => $data['content'], 'note' => $this->getContent($request),
'in_reply_to' => $data['in-reply-to'], 'in_reply_to' => $this->getInReplyTo($request),
'client_id' => $data['client-id'], 'client_id' => $client,
] ]
); );
if (array_key_exists('published', $data) && empty($data['published']) === false) { if ($this->getPublished($request)) {
$note->created_at = $note->updated_at = carbon($data['published']) $note->created_at = $note->updated_at = $this->getPublished($request);
->toDateTimeString();
} }
if (array_key_exists('location', $data) && $data['location'] !== null && $data['location'] !== 'no-location') { $note->location = $this->getLocation($request);
if (starts_with($data['location'], config('app.url'))) {
//uri of form http://host/places/slug, we want slug if ($this->getCheckin($request)) {
//get the URL path, then take last part, we can hack with basename $note->place()->associate($this->getCheckin($request));
//as path looks like file path. $note->swarm_url = $this->getSwarmUrl($request);
$place = Place::where('slug', basename(parse_url($data['location'], PHP_URL_PATH)))->first(); if ($note->note === null || $note->note == '') {
$note->place()->associate($place); $note->note = 'Ive just checked in with Swarm';
}
if (substr($data['location'], 0, 4) == 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$data['location'],
$matches
);
$note->location = $matches[0][0] . ', ' . $matches[0][1];
} }
} }
if (array_key_exists('checkin', $data) && $data['checkin'] !== null) { $note->instagram_url = $this->getInstagramUrl($request);
$place = Place::where('slug', basename(parse_url($data['checkin'], PHP_URL_PATH)))->first();
if ($place !== null) {
$note->place()->associate($place);
$note->swarm_url = $data['swarm-url'];
if ($note->note === null || $note->note == '') {
$note->note = 'Ive just checked in with Swarm';
}
}
}
/* drop image support for now foreach ($this->getMedia($request) as $media) {
//add images to media library $note->media()->save($media);
if ($request->hasFile('photo')) {
$files = $request->file('photo');
foreach ($files as $file) {
$note->addMedia($file)->toCollectionOnDisk('images', 's3');
}
}
*/
//add support for media uploaded as URLs
if (array_key_exists('photo', $data)) {
foreach ((array) $data['photo'] as $photo) {
// check the media was uploaded to my endpoint, and use path
if (starts_with($photo, config('filesystems.disks.s3.url'))) {
$path = substr($photo, strlen(config('filesystems.disks.s3.url')));
$media = Media::where('path', ltrim($path, '/'))->firstOrFail();
} else {
$media = Media::firstOrNew(['path' => $photo]);
// currently assuming this is a photo from Swarm or OwnYourGram
$media->type = 'image';
$media->save();
}
$note->media()->save($media);
}
if (array_key_exists('instagram-url', $data)) {
$note->instagram_url = $data['instagram-url'];
}
} }
$note->save(); $note->save();
@ -204,15 +51,175 @@ class NoteService
dispatch(new SendWebMentions($note)); dispatch(new SendWebMentions($note));
//syndication targets //syndication targets
if (array_key_exists('syndicate', $data)) { if (count($this->getSyndicationTargets($request)) > 0) {
if (in_array('twitter', $data['syndicate'])) { if (in_array('twitter', $this->getSyndicationTargets($request))) {
dispatch(new SyndicateNoteToTwitter($note)); dispatch(new SyndicateNoteToTwitter($note));
} }
if (in_array('facebook', $data['syndicate'])) { if (in_array('facebook', $this->getSyndicationTargets($request))) {
dispatch(new SyndicateNoteToFacebook($note)); dispatch(new SyndicateNoteToFacebook($note));
} }
} }
return $note; return $note;
} }
private function getContent(array $request): ?string
{
if (array_get($request, 'properties.content.0.html')) {
return array_get($request, 'properties.content.0.html');
}
if (is_string(array_get($request, 'properties.content.0'))) {
return array_get($request, 'properties.content.0');
}
return array_get($request, 'content');
}
private function getInReplyTo(array $request): ?string
{
if (array_get($request, 'properties.in-reply-to.0')) {
return array_get($request, 'properties.in-reply-to.0');
}
return array_get($request, 'in-reply-to');
}
private function getPublished(array $request): ?string
{
if (array_get($request, 'properties.published.0')) {
return carbon(array_get($request, 'properties.published.0'))
->toDateTimeString();
}
if (array_get($request, 'published')) {
return carbon(array_get($request, 'published'))->toDateTimeString();
}
return null;
}
private function getLocation(array $request): ?string
{
$location = array_get($request, 'properties.location.0') ?? array_get($request, 'location');
if (is_string($location) && substr($location, 0, 4) == 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$location,
$matches
);
return $matches[0][0] . ', ' . $matches[0][1];
}
return null;
}
private function getCheckin(array $request): ?Place
{
if (array_get($request, 'properties.location.0.type.0') === 'h-card') {
try {
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
array_get($request, 'properties.location.0')
);
} catch (\InvalidArgumentException $e) {
return null;
}
return $place;
}
if (starts_with(array_get($request, 'properties.location.0'), config('app.url'))) {
return Place::where(
'slug',
basename(
parse_url(
array_get($request, 'properties.location.0'),
PHP_URL_PATH
)
)
)->first();
}
if (array_get($request, 'properties.checkin')) {
try {
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
array_get($request, 'properties.checkin.0')
);
} catch (\InvalidArgumentException $e) {
return null;
}
return $place;
}
return null;
}
private function getSwarmUrl(array $request): ?string
{
if (stristr(array_get($request, 'properties.syndication.0', ''), 'swarmapp')) {
return array_get($request, 'properties.syndication.0');
}
return null;
}
private function getSyndicationTargets(array $request): array
{
$syndication = [];
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
$mpSyndicateTo = array_get($request, 'mp-syndicate-to') ?? array_get($request, 'properties.mp-syndicate-to');
if (is_string($mpSyndicateTo)) {
$service = array_search($mpSyndicateTo, $targets);
if ($service == 'Twitter') {
$syndication[] = 'twitter';
}
if ($service == 'Facebook') {
$syndication[] = 'facebook';
}
}
if (is_array($mpSyndicateTo)) {
foreach ($mpSyndicateTo as $uid) {
$service = array_search($uid, $targets);
if ($service == 'Twitter') {
$syndication[] = 'twitter';
}
if ($service == 'Facebook') {
$syndication[] = 'facebook';
}
}
}
return $syndication;
}
private function getMedia(array $request): array
{
$media = [];
$photos = array_get($request, 'photo') ?? array_get($request, 'properties.photo');
if (isset($photos)) {
foreach ((array) $photos as $photo) {
// check the media was uploaded to my endpoint, and use path
if (starts_with($photo, config('filesystems.disks.s3.url'))) {
$path = substr($photo, strlen(config('filesystems.disks.s3.url')));
$media[] = Media::where('path', ltrim($path, '/'))->firstOrFail();
} else {
$newMedia = Media::firstOrNew(['path' => $photo]);
// currently assuming this is a photo from Swarm or OwnYourGram
$newMedia->type = 'image';
$newMedia->save();
$media[] = $newMedia;
}
}
}
return $media;
}
private function getInstagramUrl(array $request): ?string
{
if (starts_with(array_get($request, 'properties.syndication.0'), 'https://www.instagram.com')) {
return array_get($request, 'properties.syndication.0');
}
return null;
}
} }

View file

@ -19,7 +19,7 @@ class PlaceService
{ {
//obviously a place needs a lat/lng, but this could be sent in a geo-url //obviously a place needs a lat/lng, but this could be sent in a geo-url
//if no geo array key, we assume the array already has lat/lng values //if no geo array key, we assume the array already has lat/lng values
if (array_key_exists('geo', $data)) { if (array_key_exists('geo', $data) && $data['geo'] !== null) {
preg_match_all( preg_match_all(
'/([0-9\.\-]+)/', '/([0-9\.\-]+)/',
$data['geo'], $data['geo'],
@ -47,7 +47,7 @@ class PlaceService
{ {
//check if the place exists if from swarm //check if the place exists if from swarm
if (array_has($checkin, 'properties.url')) { if (array_has($checkin, 'properties.url')) {
$place = Place::whereExternalURL($checkin['properties']['url'][0])->get(); $place = Place::whereExternalURL(array_get($checkin, 'properties.url.0'))->get();
if (count($place) === 1) { if (count($place) === 1) {
return $place->first(); return $place->first();
} }
@ -59,11 +59,11 @@ class PlaceService
throw new \InvalidArgumentException('Missing required longitude/latitude'); throw new \InvalidArgumentException('Missing required longitude/latitude');
} }
$place = new Place(); $place = new Place();
$place->name = $checkin['properties']['name'][0]; $place->name = array_get($checkin, 'properties.name.0');
$place->external_urls = $checkin['properties']['url'][0]; $place->external_urls = array_get($checkin, 'properties.url.0');
$place->location = new Point( $place->location = new Point(
(float) $checkin['properties']['latitude'][0], (float) array_get($checkin, 'properties.latitude.0'),
(float) $checkin['properties']['longitude'][0] (float) array_get($checkin, 'properties.longitude.0')
); );
$place->save(); $place->save();

View file

@ -2,14 +2,21 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Media;
use App\Place;
use Carbon\Carbon;
use Tests\TestCase; use Tests\TestCase;
use Tests\TestToken; use Tests\TestToken;
use Lcobucci\JWT\Builder; use Lcobucci\JWT\Builder;
use App\Jobs\ProcessMedia; use App\Jobs\ProcessMedia;
use App\Jobs\SendWebMentions;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use App\Jobs\SyndicateNoteToTwitter;
use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Hmac\Sha256;
use App\Jobs\SyndicateNoteToFacebook;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Phaza\LaravelPostgis\Geometries\Point;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
class MicropubControllerTest extends TestCase class MicropubControllerTest extends TestCase
@ -124,7 +131,9 @@ class MicropubControllerTest extends TestCase
'/api/post', '/api/post',
[ [
'h' => 'entry', 'h' => 'entry',
'content' => $note 'content' => $note,
'published' => Carbon::now()->toW3CString(),
'location' => 'geo:1.23,4.56',
], ],
[], [],
[], [],
@ -134,6 +143,60 @@ class MicropubControllerTest extends TestCase
$this->assertDatabaseHas('notes', ['note' => $note]); $this->assertDatabaseHas('notes', ['note' => $note]);
} }
/**
* Test a valid micropub requests creates a new note and syndicates to Twitter.
*
* @return void
*/
public function test_micropub_post_request_creates_new_note_sends_to_twitter()
{
Queue::fake();
$faker = \Faker\Factory::create();
$note = $faker->text;
$response = $this->call(
'POST',
'/api/post',
[
'h' => 'entry',
'content' => $note,
'mp-syndicate-to' => 'https://twitter.com/jonnybarnes'
],
[],
[],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('notes', ['note' => $note]);
Queue::assertPushed(SyndicateNoteToTwitter::class);
}
/**
* Test a valid micropub requests creates a new note and syndicates to Facebook.
*
* @return void
*/
public function test_micropub_post_request_creates_new_note_sends_to_facebook()
{
Queue::fake();
$faker = \Faker\Factory::create();
$note = $faker->text;
$response = $this->call(
'POST',
'/api/post',
[
'h' => 'entry',
'content' => $note,
'mp-syndicate-to' => 'https://facebook.com/jonnybarnes'
],
[],
[],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('notes', ['note' => $note]);
Queue::assertPushed(SyndicateNoteToFacebook::class);
}
/** /**
* Test a valid micropub requests creates a new place. * Test a valid micropub requests creates a new place.
* *
@ -182,12 +245,135 @@ class MicropubControllerTest extends TestCase
$this->assertDatabaseHas('places', ['slug' => 'the-barton-arms']); $this->assertDatabaseHas('places', ['slug' => 'the-barton-arms']);
} }
public function test_micropub_post_request_with_invalid_token_returns_expected_error_response()
{
$response = $this->call(
'POST',
'/api/post',
[
'h' => 'entry',
'content' => 'A random note',
],
[],
[],
['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()]
);
$response->assertStatus(400);
$response->assertJson(['error' => 'invalid_token']);
}
public function test_micropub_post_request_with_scopeless_token_returns_expected_error_response()
{
$response = $this->call(
'POST',
'/api/post',
[
'h' => 'entry',
'content' => 'A random note',
],
[],
[],
['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithNoScope()]
);
$response->assertStatus(400);
$response->assertJson(['error_description' => 'The provided token has no scopes']);
}
public function test_micropub_post_request_for_place_without_create_scope_errors()
{
$response = $this->call(
'POST',
'/api/post',
[
'h' => 'card',
'name' => 'The Barton Arms',
'geo' => 'geo:53.4974,-2.3768'
],
[],
[],
['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()]
);
$response->assertStatus(401);
$response->assertJson(['error' => 'insufficient_scope']);
}
/** /**
* Test a valid micropub requests using JSON syntax creates a new note. * Test a valid micropub requests using JSON syntax creates a new note.
* *
* @return void * @return void
*/ */
public function test_micropub_post_request_with_json_syntax_creates_new_note() public function test_micropub_post_request_with_json_syntax_creates_new_note()
{
Queue::fake();
Media::create([
'path' => 'test-photo.jpg',
'type' => 'image',
]);
$faker = \Faker\Factory::create();
$note = $faker->text;
$response = $this->json(
'POST',
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
'in-reply-to' => ['https://aaronpk.localhost'],
'mp-syndicate-to' => [
'https://twitter.com/jonnybarnes',
'https://facebook.com/jonnybarnes',
],
'photo' => [config('filesystems.disks.s3.url') . '/test-photo.jpg'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertStatus(201)
->assertJson(['response' => 'created']);
Queue::assertPushed(SendWebMentions::class);
Queue::assertPushed(SyndicateNoteToTwitter::class);
Queue::assertPushed(SyndicateNoteToFacebook::class);
}
/**
* Test a valid micropub requests using JSON syntax creates a new note with
* existing self-created place.
*
* @return void
*/
public function test_micropub_post_request_with_json_syntax_creates_new_note_with_existing_place_in_location()
{
$place = new Place();
$place->name = 'Test Place';
$place->location = new Point((float) 1.23, (float) 4.56);
$place->save();
$faker = \Faker\Factory::create();
$note = $faker->text;
$response = $this->json(
'POST',
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
'location' => [$place->longurl],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertStatus(201)
->assertJson(['response' => 'created']);
}
/**
* Test a valid micropub requests using JSON syntax creates a new note with
* a new place defined in the location block.
*
* @return void
*/
public function test_micropub_post_request_with_json_syntax_creates_new_note_with_new_place_in_location()
{ {
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
$note = $faker->text; $note = $faker->text;
@ -198,6 +384,14 @@ class MicropubControllerTest extends TestCase
'type' => ['h-entry'], 'type' => ['h-entry'],
'properties' => [ 'properties' => [
'content' => [$note], 'content' => [$note],
'location' => [[
'type' => ['h-card'],
'properties' => [
'name' => ['Awesome Venue'],
'latitude' => ['1.23'],
'longitude' => ['4.56'],
],
]],
], ],
], ],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()] ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
@ -205,6 +399,44 @@ class MicropubControllerTest extends TestCase
$response $response
->assertStatus(201) ->assertStatus(201)
->assertJson(['response' => 'created']); ->assertJson(['response' => 'created']);
$this->assertDatabaseHas('places', [
'name' => 'Awesome Venue',
]);
}
/**
* Test a valid micropub requests using JSON syntax creates a new note without
* a new place defined in the location block if there is missing data.
*
* @return void
*/
public function test_micropub_post_request_with_json_syntax_creates_new_note_without_new_place_in_location()
{
$faker = \Faker\Factory::create();
$note = $faker->text;
$response = $this->json(
'POST',
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
'location' => [[
'type' => ['h-card'],
'properties' => [
'name' => ['Awesome Venue'],
],
]],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertStatus(201)
->assertJson(['response' => 'created']);
$this->assertDatabaseMissing('places', [
'name' => 'Awesome Venue',
]);
} }
/** /**
@ -236,12 +468,12 @@ class MicropubControllerTest extends TestCase
} }
/** /**
* Test a micropub requests using JSON syntax without a valis token returns * Test a micropub requests using JSON syntax without a valid token returns
* an error. Also check the message. * an error. Also check the message.
* *
* @return void * @return void
*/ */
public function test_micropub_post_request_with_json_syntax_with_invalid_token_returns_error() public function test_micropub_post_request_with_json_syntax_with_insufficient_token_returns_error()
{ {
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
$note = $faker->text; $note = $faker->text;
@ -254,7 +486,7 @@ class MicropubControllerTest extends TestCase
'content' => [$note], 'content' => [$note],
], ],
], ],
['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()]
); );
$response $response
->assertJson([ ->assertJson([
@ -264,6 +496,27 @@ class MicropubControllerTest extends TestCase
->assertStatus(401); ->assertStatus(401);
} }
public function test_micropub_post_request_with_json_syntax_for_unsupported_type_returns_error()
{
$response = $this->json(
'POST',
'/api/post',
[
'type' => ['h-unsopported'], // a request type I dont support
'properties' => [
'content' => ['Some content'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson([
'response' => 'error',
'error_description' => 'unsupported_request_type'
])
->assertStatus(500);
}
public function test_micropub_post_request_with_json_syntax_creates_new_place() public function test_micropub_post_request_with_json_syntax_creates_new_place()
{ {
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
@ -332,7 +585,10 @@ class MicropubControllerTest extends TestCase
'action' => 'update', 'action' => 'update',
'url' => config('app.url') . '/notes/A', 'url' => config('app.url') . '/notes/A',
'add' => [ 'add' => [
'syndication' => ['https://www.swarmapp.com/checkin/123'], 'syndication' => [
'https://www.swarmapp.com/checkin/123',
'https://www.facebook.com/checkin/123',
],
], ],
], ],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()] ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
@ -341,7 +597,132 @@ class MicropubControllerTest extends TestCase
->assertJson(['response' => 'updated']) ->assertJson(['response' => 'updated'])
->assertStatus(200); ->assertStatus(200);
$this->assertDatabaseHas('notes', [ $this->assertDatabaseHas('notes', [
'swarm_url' => 'https://www.swarmapp.com/checkin/123' 'swarm_url' => 'https://www.swarmapp.com/checkin/123',
'facebook_url' => 'https://www.facebook.com/checkin/123',
]);
}
public function test_micropub_post_request_with_json_syntax_update_add_image_to_post()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/A',
'add' => [
'photo' => ['https://example.org/photo.jpg'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'updated'])
->assertStatus(200);
$this->assertDatabaseHas('media_endpoint', [
'path' => 'https://example.org/photo.jpg',
]);
}
public function test_micropub_post_request_with_json_syntax_update_add_post_errors_for_non_note()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/blog/A',
'add' => [
'syndication' => ['https://www.swarmapp.com/checkin/123'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['error' => 'invalid'])
->assertStatus(500);
}
public function test_micropub_post_request_with_json_syntax_update_add_post_errors_for_note_not_found()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/ZZZZ',
'add' => [
'syndication' => ['https://www.swarmapp.com/checkin/123'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['error' => 'invalid_request'])
->assertStatus(404);
}
public function test_micropub_post_request_with_json_syntax_update_add_post_errors_for_unsupported_request()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/A',
'morph' => [ // or any other unsupported update type
'syndication' => ['https://www.swarmapp.com/checkin/123'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'error'])
->assertStatus(500);
}
public function test_micropub_post_request_with_json_syntax_update_errors_for_insufficient_scope()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/B',
'add' => [
'syndication' => ['https://www.swarmapp.com/checkin/123'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()]
);
$response
->assertStatus(401)
->assertJson(['error' => 'insufficient_scope']);
}
public function test_micropub_post_request_with_json_syntax_update_replace_post_syndication()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/L',
'replace' => [
'syndication' => [
'https://www.swarmapp.com/checkin/the-id',
'https://www.facebook.com/post/the-id',
],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'updated'])
->assertStatus(200);
$this->assertDatabaseHas('notes', [
'swarm_url' => 'https://www.swarmapp.com/checkin/the-id',
'facebook_url' => 'https://www.facebook.com/post/the-id',
]); ]);
} }
@ -359,6 +740,20 @@ class MicropubControllerTest extends TestCase
$response->assertJsonFragment(['error_description' => 'The provided token did not pass validation']); $response->assertJsonFragment(['error_description' => 'The provided token did not pass validation']);
} }
public function test_media_endpoint_request_with_token_with_no_scope_returns_400_response()
{
$response = $this->call(
'POST',
'/api/media',
[],
[],
[],
['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithNoScope()]
);
$response->assertStatus(400);
$response->assertJsonFragment(['error_description' => 'The provided token has no scopes']);
}
public function test_media_endpoint_request_with_insufficient_token_scopes_returns_401_response() public function test_media_endpoint_request_with_insufficient_token_scopes_returns_401_response()
{ {
$response = $this->call( $response = $this->call(
@ -367,7 +762,7 @@ class MicropubControllerTest extends TestCase
[], [],
[], [],
[], [],
['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()]
); );
$response->assertStatus(401); $response->assertStatus(401);
$response->assertJsonFragment(['error_description' => 'The tokens scope does not have the necessary requirements.']); $response->assertJsonFragment(['error_description' => 'The tokens scope does not have the necessary requirements.']);
@ -394,4 +789,91 @@ class MicropubControllerTest extends TestCase
Queue::assertPushed(ProcessMedia::class); Queue::assertPushed(ProcessMedia::class);
Storage::disk('local')->assertExists($filename); Storage::disk('local')->assertExists($filename);
} }
public function test_media_endpoint_upload_an_audio_file()
{
Queue::fake();
Storage::fake('local');
$file = __DIR__ . '/../audio.mp3';
$response = $this->call(
'POST',
'/api/media',
[],
[],
[
'file' => new UploadedFile($file, 'audio.mp3', 'audio/mpeg', filesize($file), null, true),
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$path = parse_url($response->getData()->location, PHP_URL_PATH);
$filename = substr($path, 7);
Queue::assertPushed(ProcessMedia::class);
Storage::disk('local')->assertExists($filename);
}
public function test_media_endpoint_upload_a_video_file()
{
Queue::fake();
Storage::fake('local');
$file = __DIR__ . '/../video.ogv';
$response = $this->call(
'POST',
'/api/media',
[],
[],
[
'file' => new UploadedFile($file, 'video.ogv', 'video/ogg', filesize($file), null, true),
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$path = parse_url($response->getData()->location, PHP_URL_PATH);
$filename = substr($path, 7);
Queue::assertPushed(ProcessMedia::class);
Storage::disk('local')->assertExists($filename);
}
public function test_media_endpoint_upload_a_document_file()
{
Queue::fake();
Storage::fake('local');
$response = $this->call(
'POST',
'/api/media',
[],
[],
[
'file' => UploadedFile::fake()->create('document.pdf', 100),
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$path = parse_url($response->getData()->location, PHP_URL_PATH);
$filename = substr($path, 7);
Queue::assertPushed(ProcessMedia::class);
Storage::disk('local')->assertExists($filename);
}
public function test_media_endpoint_upload_an_invalid_file_return_error()
{
Queue::fake();
Storage::fake('local');
$response = $this->call(
'POST',
'/api/media',
[],
[],
[
'file' => new UploadedFile(__DIR__ . '/../aaron.png', 'aaron.png', 'image/png', UPLOAD_ERR_INI_SIZE, true),
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response->assertStatus(400);
$response->assertJson(['error_description' => 'The uploaded file failed validation']);
}
} }

View file

@ -153,4 +153,37 @@ class SwarmTest extends TestCase
'swarm_url' => 'https://www.swarmapp.com/checkin/def' 'swarm_url' => 'https://www.swarmapp.com/checkin/def'
]); ]);
} }
public function test_faked_ownyourswarm_request_saves_just_post_when_error_in_checkin_data()
{
$response = $this->json(
'POST',
'api/post',
[
'type' => ['h-entry'],
'properties' => [
'published' => [\Carbon\Carbon::now()->toDateTimeString()],
'syndication' => ['https://www.swarmapp.com/checkin/abc'],
'content' => [[
'value' => 'My first #checkin using Example Product',
'html' => 'My first #checkin using <a href="http://example.org">Example Product</a>',
]],
'checkin' => [[
'type' => ['h-card'],
'properties' => [
'name' => ['Awesome Venue'],
'url' => ['https://foursquare.com/v/123456'],
],
]],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertStatus(201)
->assertJson(['response' => 'created']);
$this->assertDatabaseMissing('places', [
'name' => 'Awesome Venue',
]);
}
} }

View file

@ -32,4 +32,46 @@ class TokenEndpointTest extends TestCase
$this->assertEquals(config('app.url'), $output['me']); $this->assertEquals(config('app.url'), $output['me']);
$this->assertTrue(array_key_exists('access_token', $output)); $this->assertTrue(array_key_exists('access_token', $output));
} }
public function test_token_endpoint_returns_error_when_auth_endpoint_lacks_me_data()
{
$mockClient = Mockery::mock(Client::class);
$mockClient->shouldReceive('discoverAuthorizationEndpoint')
->with(normalize_url(config('app.url')))
->once()
->andReturn('https://indieauth.com/auth');
$mockClient->shouldReceive('verifyIndieAuthCode')
->andReturn([
'error' => 'error_message',
]);
$this->app->instance(Client::class, $mockClient);
$response = $this->post('/api/token', [
'me' => config('app.url'),
'code' => 'abc123',
'redirect_uri' => config('app.url') . '/indieauth-callback',
'client_id' => config('app.url') . '/micropub-client',
'state' => mt_rand(1000, 10000),
]);
$response->assertStatus(400);
$response->assertSeeText('There was an error verifying the authorisation code.');
}
public function test_token_endpoint_returns_error_when_no_auth_endpoint_found()
{
$mockClient = Mockery::mock(Client::class);
$mockClient->shouldReceive('discoverAuthorizationEndpoint')
->with(normalize_url(config('app.url')))
->once()
->andReturn(null);
$this->app->instance(Client::class, $mockClient);
$response = $this->post('/api/token', [
'me' => config('app.url'),
'code' => 'abc123',
'redirect_uri' => config('app.url') . '/indieauth-callback',
'client_id' => config('app.url') . '/micropub-client',
'state' => mt_rand(1000, 10000),
]);
$response->assertStatus(400);
$response->assertSeeText('Cant determine the authorisation endpoint.');
}
} }

View file

@ -21,7 +21,7 @@ trait TestToken
return $token; return $token;
} }
public function getInvalidToken() public function getTokenWithIncorrectScope()
{ {
$signer = new Sha256(); $signer = new Sha256();
$token = (new Builder()) $token = (new Builder())
@ -34,4 +34,24 @@ trait TestToken
return $token; return $token;
} }
public function getTokenWithNoScope()
{
$signer = new Sha256();
$token = (new Builder())
->set('client_id', 'https://quill.p3k.io')
->set('me', 'https://jonnybarnes.localhost')
->set('issued_at', time())
->sign($signer, env('APP_KEY'))
->getToken();
return $token;
}
public function getInvalidToken()
{
$token = $this->getToken();
return substr($token, 0, -5);
}
} }

BIN
tests/audio.mp3 Normal file

Binary file not shown.

BIN
tests/video.ogv Normal file

Binary file not shown.