mirror of https://github.com/pixelfed/pixelfed
test: add status, timeline, federation, and privacy tests (309 total)
Api/StatusTest: get/delete statuses, favourite/unfavourite, bookmark, status creation validation, ownership checks Api/TimelineTest: public/home/hashtag timelines, private exclusion, pagination support Federation/NodeInfoTest: nodeinfo, webfinger, host-meta endpoints Account/PrivacyTest: private profile visibility, blocked user access, privacy settings togglepull/6858/head
parent
891e282808
commit
e1f883a41b
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
uses(LazilyRefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Account Privacy Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
describe('private profiles', function () {
|
||||
it('hides statuses from non-followers on private profile', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$private = User::factory()->create();
|
||||
$private->refresh();
|
||||
$private->profile->update(['is_private' => true]);
|
||||
|
||||
Status::factory()->create([
|
||||
'profile_id' => $private->profile_id,
|
||||
'type' => 'photo',
|
||||
'scope' => 'private',
|
||||
'visibility' => 'private',
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson("/api/v1/accounts/{$private->profile_id}/statuses")
|
||||
->assertOk()
|
||||
->assertJson([]);
|
||||
});
|
||||
|
||||
it('shows statuses to followers of a private profile', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$private = User::factory()->create();
|
||||
$private->refresh();
|
||||
$private->profile->update(['is_private' => true]);
|
||||
|
||||
Status::factory()->create([
|
||||
'profile_id' => $private->profile_id,
|
||||
'type' => 'photo',
|
||||
'scope' => 'private',
|
||||
'visibility' => 'private',
|
||||
]);
|
||||
|
||||
Follower::create([
|
||||
'profile_id' => $user->profile_id,
|
||||
'following_id' => $private->profile_id,
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson("/api/v1/accounts/{$private->profile_id}/statuses")
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
describe('blocked users', function () {
|
||||
it('returns empty when viewing a profile that blocked you', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$blocker = User::factory()->create();
|
||||
$blocker->refresh();
|
||||
|
||||
Status::factory()->count(3)->create([
|
||||
'profile_id' => $blocker->profile_id,
|
||||
'type' => 'photo',
|
||||
'scope' => 'public',
|
||||
]);
|
||||
|
||||
// Blocker blocks the user
|
||||
\App\UserFilter::create([
|
||||
'user_id' => $blocker->profile_id,
|
||||
'filterable_id' => $user->profile_id,
|
||||
'filterable_type' => Profile::class,
|
||||
'filter_type' => 'block',
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson("/api/v1/accounts/{$blocker->profile_id}/statuses")
|
||||
->assertOk()
|
||||
->assertJson([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('privacy settings', function () {
|
||||
it('toggles account privacy to private', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('/settings/privacy', [
|
||||
'is_private' => 'on',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$user->profile->refresh();
|
||||
expect((bool) $user->profile->is_private)->toBeTrue();
|
||||
});
|
||||
|
||||
it('toggles account privacy to public', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$user->profile->update(['is_private' => true]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('/settings/privacy', [])
|
||||
->assertRedirect();
|
||||
|
||||
$user->profile->refresh();
|
||||
expect((bool) $user->profile->is_private)->toBeFalse();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
use App\Like;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
uses(LazilyRefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Status API Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
describe('GET /api/v1/statuses/{id}', function () {
|
||||
it('returns a public status by id', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$status = Status::factory()->create([
|
||||
'profile_id' => $user->profile_id,
|
||||
'type' => 'photo',
|
||||
'scope' => 'public',
|
||||
'visibility' => 'public',
|
||||
]);
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson("/api/v1/statuses/{$status->id}")
|
||||
->assertOk()
|
||||
->assertJsonFragment(['id' => (string) $status->id]);
|
||||
});
|
||||
|
||||
it('returns 404 for a non-existent status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson('/api/v1/statuses/999999999')
|
||||
->assertNotFound();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/v1/statuses/{id}', function () {
|
||||
it('deletes own status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$status = Status::factory()->create([
|
||||
'profile_id' => $user->profile_id,
|
||||
'type' => 'photo',
|
||||
]);
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
$this->deleteJson("/api/v1/statuses/{$status->id}")
|
||||
->assertOk();
|
||||
|
||||
expect(Status::find($status->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('cannot delete another users status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$other = User::factory()->create();
|
||||
$other->refresh();
|
||||
$status = Status::factory()->create([
|
||||
'profile_id' => $other->profile_id,
|
||||
'type' => 'photo',
|
||||
]);
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
// Returns 404 to avoid revealing existence to non-owners
|
||||
$this->deleteJson("/api/v1/statuses/{$status->id}")
|
||||
->assertNotFound();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/statuses/{id}/favourite', function () {
|
||||
it('favourites a public status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$other = User::factory()->create();
|
||||
$other->refresh();
|
||||
$status = Status::factory()->create([
|
||||
'profile_id' => $other->profile_id,
|
||||
'type' => 'photo',
|
||||
'scope' => 'public',
|
||||
]);
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
$this->postJson("/api/v1/statuses/{$status->id}/favourite")
|
||||
->assertOk()
|
||||
->assertJsonFragment(['favourited' => true]);
|
||||
|
||||
expect(Like::where('profile_id', $user->profile_id)
|
||||
->where('status_id', $status->id)
|
||||
->exists()
|
||||
)->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns 404 for non-existent status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
$this->postJson('/api/v1/statuses/999999999/favourite')
|
||||
->assertNotFound();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/statuses/{id}/unfavourite', function () {
|
||||
it('unfavourites a previously liked status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$other = User::factory()->create();
|
||||
$other->refresh();
|
||||
$status = Status::factory()->create([
|
||||
'profile_id' => $other->profile_id,
|
||||
'type' => 'photo',
|
||||
'scope' => 'public',
|
||||
]);
|
||||
|
||||
Like::create([
|
||||
'profile_id' => $user->profile_id,
|
||||
'status_id' => $status->id,
|
||||
'status_profile_id' => $other->profile_id,
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
$this->postJson("/api/v1/statuses/{$status->id}/unfavourite")
|
||||
->assertOk()
|
||||
->assertJsonFragment(['favourited' => false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/statuses/{id}/bookmark', function () {
|
||||
it('bookmarks a status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$status = Status::factory()->create(['type' => 'photo', 'scope' => 'public']);
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
$this->postJson("/api/v1/statuses/{$status->id}/bookmark")
|
||||
->assertOk()
|
||||
->assertJsonFragment(['bookmarked' => true]);
|
||||
});
|
||||
|
||||
it('unbookmarks a bookmarked status', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$status = Status::factory()->create(['type' => 'photo', 'scope' => 'public']);
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
// Bookmark first
|
||||
$this->postJson("/api/v1/statuses/{$status->id}/bookmark");
|
||||
|
||||
$this->postJson("/api/v1/statuses/{$status->id}/unbookmark")
|
||||
->assertOk()
|
||||
->assertJsonFragment(['bookmarked' => false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/statuses (create)', function () {
|
||||
it('rejects status creation without media or text', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
// The endpoint first checks token validity (403 if no token object)
|
||||
// then validates the payload. With Passport::actingAs the token()
|
||||
// method returns null, triggering the abort_if check.
|
||||
$this->postJson('/api/v1/statuses', [])
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
it('rejects direct visibility', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
Passport::actingAs($user, ['write']);
|
||||
|
||||
$this->postJson('/api/v1/statuses', [
|
||||
'status' => 'Hello world',
|
||||
'visibility' => 'direct',
|
||||
])->assertStatus(400)
|
||||
->assertJsonFragment(['error' => 'Direct visibility is not available.']);
|
||||
});
|
||||
|
||||
it('rejects status exceeding max caption length', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
Passport::actingAs($user, ['write']);
|
||||
$maxLen = (int) config('pixelfed.max_caption_length', 150);
|
||||
|
||||
$this->postJson('/api/v1/statuses', [
|
||||
'status' => str_repeat('a', $maxLen + 10),
|
||||
])->assertUnprocessable();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
use App\Follower;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
uses(LazilyRefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Timeline API Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
describe('public timeline', function () {
|
||||
it('returns public statuses', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
|
||||
Status::factory()->count(3)->create([
|
||||
'type' => 'photo',
|
||||
'scope' => 'public',
|
||||
'visibility' => 'public',
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson('/api/v1/timelines/public')
|
||||
->assertOk()
|
||||
->assertJsonIsArray();
|
||||
});
|
||||
|
||||
it('does not include private statuses in public timeline', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
|
||||
$private = Status::factory()->private()->create(['type' => 'photo']);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$response = $this->getJson('/api/v1/timelines/public');
|
||||
$ids = collect($response->json())->pluck('id')->toArray();
|
||||
|
||||
expect($ids)->not->toContain((string) $private->id);
|
||||
});
|
||||
|
||||
it('supports limit parameter', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
|
||||
Status::factory()->count(5)->create([
|
||||
'type' => 'photo',
|
||||
'scope' => 'public',
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$response = $this->getJson('/api/v1/timelines/public?limit=2');
|
||||
$response->assertOk();
|
||||
|
||||
expect(count($response->json()))->toBeLessThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('home timeline', function () {
|
||||
it('returns statuses from followed accounts', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$followed = User::factory()->create();
|
||||
$followed->refresh();
|
||||
|
||||
Follower::create([
|
||||
'profile_id' => $user->profile_id,
|
||||
'following_id' => $followed->profile_id,
|
||||
]);
|
||||
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson('/api/v1/timelines/home')
|
||||
->assertOk()
|
||||
->assertJsonIsArray();
|
||||
});
|
||||
|
||||
it('requires authentication', function () {
|
||||
$this->getJson('/api/v1/timelines/home')
|
||||
->assertUnauthorized();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashtag timeline', function () {
|
||||
it('returns statuses for a hashtag', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
Passport::actingAs($user, ['read']);
|
||||
|
||||
$this->getJson('/api/v1/timelines/tag/pixelfed')
|
||||
->assertOk()
|
||||
->assertJsonIsArray();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||
|
||||
uses(LazilyRefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Federation & Discovery Endpoint Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
describe('nodeinfo', function () {
|
||||
it('returns well-known nodeinfo with links', function () {
|
||||
$this->getJson('/.well-known/nodeinfo')
|
||||
->assertOk()
|
||||
->assertJsonStructure(['links' => [['rel', 'href']]]);
|
||||
});
|
||||
|
||||
it('returns nodeinfo 2.0 with correct structure', function () {
|
||||
$this->getJson('/api/nodeinfo/2.0.json')
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'version',
|
||||
'software' => ['name', 'version'],
|
||||
'protocols',
|
||||
'usage' => ['users', 'localPosts'],
|
||||
'openRegistrations',
|
||||
])
|
||||
->assertJsonFragment(['name' => 'pixelfed']);
|
||||
});
|
||||
|
||||
it('reports correct open registration status', function () {
|
||||
config(['pixelfed.open_registration' => true]);
|
||||
config(['instance.enable_cc' => false]);
|
||||
|
||||
$response = $this->getJson('/api/nodeinfo/2.0.json');
|
||||
$data = $response->json();
|
||||
|
||||
expect($data['openRegistrations'])->toBeBool();
|
||||
});
|
||||
});
|
||||
|
||||
describe('webfinger', function () {
|
||||
it('returns 400 without resource parameter', function () {
|
||||
$this->getJson('/.well-known/webfinger')
|
||||
->assertStatus(400);
|
||||
});
|
||||
|
||||
it('returns 400 for invalid resource format', function () {
|
||||
$this->getJson('/.well-known/webfinger?resource=invalid')
|
||||
->assertStatus(400);
|
||||
});
|
||||
|
||||
it('returns 400 for non-existent user', function () {
|
||||
$this->getJson('/.well-known/webfinger?resource=acct:nonexistent@'.config('pixelfed.domain.app'))
|
||||
->assertStatus(400);
|
||||
});
|
||||
|
||||
it('returns webfinger data for existing local user', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->refresh();
|
||||
$domain = config('pixelfed.domain.app');
|
||||
|
||||
$this->getJson("/.well-known/webfinger?resource=acct:{$user->username}@{$domain}")
|
||||
->assertOk()
|
||||
->assertJsonStructure(['subject', 'aliases', 'links']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('host-meta', function () {
|
||||
it('returns host-meta XML', function () {
|
||||
$this->get('/.well-known/host-meta')
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'application/xrd+xml');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue