jonnybarnes.uk/tests/Feature/MicropubControllerTest.php
Jonny Barnes 1dfa17abca
Some checks failed
PHP Unit / PHPUnit test suite (pull_request) Has been cancelled
Laravel Pint / Laravel Pint (pull_request) Has been cancelled
Update Laravel to v12
2025-04-01 21:10:30 +01:00

713 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Jobs\SendWebMentions;
use App\Jobs\SyndicateNoteToBluesky;
use App\Jobs\SyndicateNoteToMastodon;
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\Facades\Queue;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
use Tests\TestToken;
class MicropubControllerTest extends TestCase
{
use RefreshDatabase;
use TestToken;
#[Test]
public function micropubGetRequestWithoutTokenReturnsErrorResponse(): void
{
$response = $this->get('/api/post');
$response->assertStatus(401);
$response->assertJsonFragment(['error_description' => 'No access token was provided in the request']);
}
#[Test]
public function micropubGetRequestWithoutValidTokenReturnsErrorResponse(): void
{
$response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer abc123']);
$response->assertStatus(400);
$response->assertJsonFragment(['error_description' => 'The provided token did not pass validation']);
}
/**
* Test a GET request for the micropub endpoint with a valid token gives a
* 200 response. Check token information is also returned in the response.
*/
#[Test]
public function micropubGetRequestWithValidTokenReturnsOkResponse(): void
{
$response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertStatus(200);
$response->assertJsonFragment(['response' => 'token']);
}
#[Test]
public function micropubClientsCanRequestSyndicationTargetsCanBeEmpty(): void
{
$response = $this->get('/api/post?q=syndicate-to', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertJsonFragment(['syndicate-to' => []]);
}
#[Test]
public function micropubClientsCanRequestSyndicationTargetsPopulatesFromModel(): void
{
$syndicationTarget = SyndicationTarget::factory()->create();
$response = $this->get('/api/post?q=syndicate-to', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertJsonFragment(['uid' => $syndicationTarget->uid]);
}
#[Test]
public function micropubClientsCanRequestKnownNearbyPlaces(): void
{
Place::factory()->create([
'name' => 'The Bridgewater Pub',
'latitude' => '53.5',
'longitude' => '-2.38',
]);
$response = $this->get('/api/post?q=geo:53.5,-2.38', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertJson(['places' => [['slug' => 'the-bridgewater-pub']]]);
}
/**
* @todo Add uncertainty parameter
*
public function micropubClientsCanRequestKnownNearbyPlacesWithUncertaintyParameter(): void
{
$response = $this->get('/api/post?q=geo:53.5,-2.38', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertJson(['places' => [['slug' => 'the-bridgewater-pub']]]);
}*/
#[Test]
public function returnEmptyResultWhenMicropubClientRequestsKnownNearbyPlaces(): void
{
$response = $this->get('/api/post?q=geo:1.23,4.56', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertJson(['places' => []]);
}
#[Test]
public function micropubClientCanRequestEndpointConfig(): void
{
$response = $this->get('/api/post?q=config', ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]);
$response->assertJsonFragment(['media-endpoint' => route('media-endpoint')]);
}
#[Test]
public function micropubClientCanCreateNewNote(): void
{
$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]);
}
#[Test]
public function micropubClientCanRequestTheNewNoteIsSyndicatedToMastodonAndBluesky(): void
{
Queue::fake();
SyndicationTarget::factory()->create([
'uid' => 'https://mastodon.social/@jonnybarnes',
'service_name' => 'Mastodon',
]);
SyndicationTarget::factory()->create([
'uid' => 'https://bsky.app/profile/jonnybarnes.uk',
'service_name' => 'Bluesky',
]);
$faker = Factory::create();
$note = $faker->text;
$response = $this->post(
'/api/post',
[
'h' => 'entry',
'content' => $note,
'mp-syndicate-to' => [
'https://mastodon.social/@jonnybarnes',
'https://bsky.app/profile/jonnybarnes.uk',
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('notes', ['note' => $note]);
Queue::assertPushed(SyndicateNoteToMastodon::class);
Queue::assertPushed(SyndicateNoteToBluesky::class);
}
#[Test]
public function micropubClientsCanCreateNewPlaces(): void
{
$response = $this->post(
'/api/post',
[
'h' => 'card',
'name' => 'The Barton Arms',
'geo' => 'geo:53.4974,-2.3768',
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('places', ['slug' => 'the-barton-arms']);
}
#[Test]
public function micropubClientsCanCreateNewPlacesWithOldLocationSyntax(): void
{
$response = $this->post(
'/api/post',
[
'h' => 'card',
'name' => 'The Barton Arms',
'latitude' => '53.4974',
'longitude' => '-2.3768',
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('places', ['slug' => 'the-barton-arms']);
}
#[Test]
public function micropubClientWebRequestWithInvalidTokenReturnsErrorResponse(): void
{
$response = $this->post(
'/api/post',
[
'h' => 'entry',
'content' => 'A random note',
],
['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()]
);
$response->assertStatus(400);
$response->assertJson(['error' => 'invalid_token']);
}
#[Test]
public function micropubClientWebRequestWithTokenWithoutAnyScopesReturnsErrorResponse(): void
{
$response = $this->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']);
}
#[Test]
public function micropubClientWebRequestWithTokenWithoutCreateScopesReturnsErrorResponse(): void
{
$response = $this->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]
public function micropubClientApiRequestCreatesNewNote(): void
{
Queue::fake();
Media::create([
'path' => 'test-photo.jpg',
'type' => 'image',
]);
SyndicationTarget::factory()->create([
'uid' => 'https://mastodon.social/@jonnybarnes',
'service_name' => 'Mastodon',
]);
SyndicationTarget::factory()->create([
'uid' => 'https://bsky.app/profile/jonnybarnes.uk',
'service_name' => 'Bluesky',
]);
$faker = Factory::create();
$note = $faker->text;
$response = $this->postJson(
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
'in-reply-to' => ['https://aaronpk.localhost'],
'mp-syndicate-to' => [
'https://mastodon.social/@jonnybarnes',
'https://bsky.app/profile/jonnybarnes.uk',
],
'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(SyndicateNoteToMastodon::class);
Queue::assertPushed(SyndicateNoteToBluesky::class);
}
/**
* Test a valid micropub requests using JSON syntax creates a new note with
* existing self-created place.
*/
#[Test]
public function micropubClientApiRequestCreatesNewNoteWithExistingPlaceInLocationData(): void
{
$place = new Place;
$place->name = 'Test Place';
$place->latitude = 1.23;
$place->longitude = 4.56;
$place->save();
$faker = Factory::create();
$note = $faker->text;
$response = $this->postJson(
'/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.
*/
#[Test]
public function micropubClientApiRequestCreatesNewNoteWithNewPlaceInLocationData(): void
{
$faker = Factory::create();
$note = $faker->text;
$response = $this->postJson(
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
],
'location' => [
'type' => ['h-card'],
'properties' => [
'name' => ['Awesome Venue'],
'latitude' => ['1.23'],
'longitude' => ['4.56'],
],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertStatus(201)
->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.
*/
#[Test]
public function micropubClientApiRequestCreatesNewNoteWithoutNewPlaceInLocationData(): void
{
$faker = Factory::create();
$note = $faker->text;
$response = $this->postJson(
'/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',
]);
}
/**
* Test a micropub requests using JSON syntax without a token returns an
* error. Also check the message.
*/
#[Test]
public function micropubClientApiRequestWithoutTokenReturnsError(): void
{
$faker = Factory::create();
$note = $faker->text;
$response = $this->postJson(
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
],
]
);
$response
->assertJson([
'response' => 'error',
'error' => 'unauthorized',
])
->assertStatus(401);
}
/**
* Test a micropub requests using JSON syntax without a valid token returns
* an error. Also check the message.
*/
#[Test]
public function micropubClientApiRequestWithTokenWithInsufficientPermissionReturnsError(): void
{
$faker = Factory::create();
$note = $faker->text;
$response = $this->postJson(
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'content' => [$note],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()]
);
$response
->assertJson([
'response' => 'error',
'error' => 'insufficient_scope',
])
->assertStatus(401);
}
#[Test]
public function micropubClientApiRequestForUnsupportedPostTypeReturnsError(): void
{
$response = $this->postJson(
'/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);
}
#[Test]
public function micropubClientApiRequestCreatesNewPlace(): void
{
$faker = Factory::create();
$response = $this->postJson(
'/api/post',
[
'type' => ['h-card'],
'properties' => [
'name' => $faker->name,
'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude,
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'created'])
->assertStatus(201);
}
#[Test]
public function micropubClientApiRequestCreatesNewPlaceWithUncertaintyParameter(): void
{
$faker = Factory::create();
$response = $this->postJson(
'/api/post',
[
'type' => ['h-card'],
'properties' => [
'name' => $faker->name,
'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude . ';u=35',
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'created'])
->assertStatus(201);
}
#[Test]
public function micropubClientApiRequestUpdatesExistingNote(): void
{
$note = Note::factory()->create();
$response = $this->postJson(
'/api/post',
[
'action' => 'update',
'url' => $note->longurl,
'replace' => [
'content' => ['replaced content'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'updated'])
->assertStatus(200);
}
#[Test]
public function micropubClientApiRequestUpdatesNoteSyndicationLinks(): void
{
$note = Note::factory()->create();
$response = $this->postJson(
'/api/post',
[
'action' => 'update',
'url' => $note->longurl,
'add' => [
'syndication' => [
'https://www.swarmapp.com/checkin/123',
'https://www.facebook.com/checkin/123',
],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'updated'])
->assertStatus(200);
$this->assertDatabaseHas('notes', [
'swarm_url' => 'https://www.swarmapp.com/checkin/123',
'facebook_url' => 'https://www.facebook.com/checkin/123',
]);
}
#[Test]
public function micropubClientApiRequestAddsImageToNote(): void
{
$note = Note::factory()->create();
$response = $this->postJson(
'/api/post',
[
'action' => 'update',
'url' => $note->longurl,
'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',
]);
}
#[Test]
public function micropubClientApiRequestReturnsErrorTryingToUpdateNonNoteModel(): void
{
$response = $this->postJson(
'/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);
}
#[Test]
public function micropubClientApiRequestReturnsErrorTryingToUpdateNonExistingNote(): void
{
$response = $this->postJson(
'/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);
}
#[Test]
public function micropubClientApiRequestReturnsErrorWhenTryingToUpdateUnsupportedProperty(): void
{
$note = Note::factory()->create();
$response = $this->postJson(
'/api/post',
[
'action' => 'update',
'url' => $note->longurl,
'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);
}
#[Test]
public function micropubClientApiRequestWithTokenWithInsufficientScopeReturnsError(): void
{
$response = $this->postJson(
'/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']);
}
#[Test]
public function micropubClientApiRequestCanReplaceNoteSyndicationTargets(): void
{
$note = Note::factory()->create();
$response = $this->postJson(
'/api/post',
[
'action' => 'update',
'url' => $note->longurl,
'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',
]);
}
#[Test]
public function micropubClientWebRequestCanEncodeTokenWithinTheForm(): void
{
$faker = Factory::create();
$note = $faker->text;
$response = $this->post(
'/api/post',
[
'h' => 'entry',
'content' => $note,
'published' => Carbon::now()->toW3CString(),
'access_token' => (string) $this->getToken(),
]
);
$response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('notes', ['note' => $note]);
}
#[Test]
public function micropubClientApiRequestCreatesArticlesWhenItIncludesTheNameProperty(): void
{
$faker = Factory::create();
$name = $faker->text(50);
$content = $faker->paragraphs(5, true);
$response = $this->postJson(
'/api/post',
[
'type' => ['h-entry'],
'properties' => [
'name' => $name,
'content' => $content,
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'created'])
->assertStatus(201);
$this->assertDatabaseHas('articles', [
'title' => $name,
'main' => $content,
]);
}
}