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 toggle
pull/6858/head
Your Name 4 weeks ago
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…
Cancel
Save