25 - Fix all PHPStan errors and add mockery extension
Some checks failed
CI / ci (push) Failing after 4m31s

This commit is contained in:
myrmidex 2026-03-08 14:18:28 +01:00
parent 56db303b15
commit 6784af2ff6
118 changed files with 1398 additions and 1235 deletions

View file

@ -23,6 +23,8 @@ public function sendResponse(mixed $result, string $message = 'Success', int $co
/** /**
* Error response method * Error response method
*
* @param array<string, mixed> $errorMessages
*/ */
public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse
{ {
@ -31,7 +33,7 @@ public function sendError(string $error, array $errorMessages = [], int $code =
'message' => $error, 'message' => $error,
]; ];
if (!empty($errorMessages)) { if (! empty($errorMessages)) {
$response['errors'] = $errorMessages; $response['errors'] = $errorMessages;
} }
@ -40,6 +42,8 @@ public function sendError(string $error, array $errorMessages = [], int $code =
/** /**
* Validation error response method * Validation error response method
*
* @param array<string, mixed> $errors
*/ */
public function sendValidationError(array $errors): JsonResponse public function sendValidationError(array $errors): JsonResponse
{ {
@ -61,4 +65,4 @@ public function sendUnauthorized(string $message = 'Unauthorized'): JsonResponse
{ {
return $this->sendError($message, [], 401); return $this->sendError($message, [], 401);
} }
} }

View file

@ -3,9 +3,6 @@
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Models\Article; use App\Models\Article;
use App\Models\Feed;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Services\DashboardStatsService; use App\Services\DashboardStatsService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -40,8 +37,7 @@ public function stats(Request $request): JsonResponse
'current_period' => $period, 'current_period' => $period,
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
throw $e; return $this->sendError('Failed to fetch dashboard stats: '.$e->getMessage(), [], 500);
return $this->sendError('Failed to fetch dashboard stats: ' . $e->getMessage(), [], 500);
} }
} }
} }

View file

@ -31,18 +31,18 @@ public function status(): JsonResponse
// 1. They haven't completed or skipped onboarding AND // 1. They haven't completed or skipped onboarding AND
// 2. They don't have all required components // 2. They don't have all required components
$hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute; $hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute;
$needsOnboarding = !$onboardingCompleted && !$onboardingSkipped && !$hasAllComponents; $needsOnboarding = ! $onboardingCompleted && ! $onboardingSkipped && ! $hasAllComponents;
// Determine current step // Determine current step
$currentStep = null; $currentStep = null;
if ($needsOnboarding) { if ($needsOnboarding) {
if (!$hasPlatformAccount) { if (! $hasPlatformAccount) {
$currentStep = 'platform'; $currentStep = 'platform';
} elseif (!$hasFeed) { } elseif (! $hasFeed) {
$currentStep = 'feed'; $currentStep = 'feed';
} elseif (!$hasChannel) { } elseif (! $hasChannel) {
$currentStep = 'channel'; $currentStep = 'channel';
} elseif (!$hasRoute) { } elseif (! $hasRoute) {
$currentStep = 'route'; $currentStep = 'route';
} }
} }
@ -56,7 +56,7 @@ public function status(): JsonResponse
'has_route' => $hasRoute, 'has_route' => $hasRoute,
'onboarding_skipped' => $onboardingSkipped, 'onboarding_skipped' => $onboardingSkipped,
'onboarding_completed' => $onboardingCompleted, 'onboarding_completed' => $onboardingCompleted,
'missing_components' => !$hasAllComponents && $onboardingCompleted, 'missing_components' => ! $hasAllComponents && $onboardingCompleted,
], 'Onboarding status retrieved successfully.'); ], 'Onboarding status retrieved successfully.');
} }
@ -84,8 +84,10 @@ public function options(): JsonResponse
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']); ->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
// Get feed providers from config // Get feed providers from config
$feedProviders = collect(config('feed.providers', [])) /** @var array<string, array<string, mixed>> $providers */
->filter(fn($provider) => $provider['is_active']) $providers = config('feed.providers', []);
$feedProviders = collect($providers)
->filter(fn (array $provider) => $provider['is_active'])
->values(); ->values();
return $this->sendResponse([ return $this->sendResponse([

View file

@ -5,8 +5,8 @@
use App\Actions\CreateChannelAction; use App\Actions\CreateChannelAction;
use App\Http\Requests\StorePlatformChannelRequest; use App\Http\Requests\StorePlatformChannelRequest;
use App\Http\Resources\PlatformChannelResource; use App\Http\Resources\PlatformChannelResource;
use App\Models\PlatformChannel;
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use Exception; use Exception;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -54,7 +54,7 @@ public function store(StorePlatformChannelRequest $request, CreateChannelAction
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
return $this->sendError($e->getMessage(), [], 422); return $this->sendError($e->getMessage(), [], 422);
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to create platform channel: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to create platform channel: '.$e->getMessage(), [], 500);
} }
} }
@ -91,7 +91,7 @@ public function update(Request $request, PlatformChannel $platformChannel): Json
} catch (ValidationException $e) { } catch (ValidationException $e) {
return $this->sendValidationError($e->errors()); return $this->sendValidationError($e->errors());
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to update platform channel: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to update platform channel: '.$e->getMessage(), [], 500);
} }
} }
@ -108,7 +108,7 @@ public function destroy(PlatformChannel $platformChannel): JsonResponse
'Platform channel deleted successfully!' 'Platform channel deleted successfully!'
); );
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to delete platform channel: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to delete platform channel: '.$e->getMessage(), [], 500);
} }
} }
@ -118,7 +118,7 @@ public function destroy(PlatformChannel $platformChannel): JsonResponse
public function toggle(PlatformChannel $channel): JsonResponse public function toggle(PlatformChannel $channel): JsonResponse
{ {
try { try {
$newStatus = !$channel->is_active; $newStatus = ! $channel->is_active;
$channel->update(['is_active' => $newStatus]); $channel->update(['is_active' => $newStatus]);
$status = $newStatus ? 'activated' : 'deactivated'; $status = $newStatus ? 'activated' : 'deactivated';
@ -128,7 +128,7 @@ public function toggle(PlatformChannel $channel): JsonResponse
"Platform channel {$status} successfully!" "Platform channel {$status} successfully!"
); );
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to toggle platform channel status: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to toggle platform channel status: '.$e->getMessage(), [], 500);
} }
} }
@ -144,6 +144,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
'priority' => 'nullable|integer|min:1|max:100', 'priority' => 'nullable|integer|min:1|max:100',
]); ]);
/** @var PlatformAccount $platformAccount */
$platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']); $platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']);
// Check if account is already attached // Check if account is already attached
@ -165,7 +166,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
} catch (ValidationException $e) { } catch (ValidationException $e) {
return $this->sendValidationError($e->errors()); return $this->sendValidationError($e->errors());
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to attach platform account: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to attach platform account: '.$e->getMessage(), [], 500);
} }
} }
@ -175,7 +176,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
public function detachAccount(PlatformChannel $channel, PlatformAccount $account): JsonResponse public function detachAccount(PlatformChannel $channel, PlatformAccount $account): JsonResponse
{ {
try { try {
if (!$channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) { if (! $channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) {
return $this->sendError('Platform account is not attached to this channel.', [], 422); return $this->sendError('Platform account is not attached to this channel.', [], 422);
} }
@ -186,7 +187,7 @@ public function detachAccount(PlatformChannel $channel, PlatformAccount $account
'Platform account detached from channel successfully!' 'Platform account detached from channel successfully!'
); );
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to detach platform account: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to detach platform account: '.$e->getMessage(), [], 500);
} }
} }
@ -201,7 +202,7 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount
'priority' => 'nullable|integer|min:1|max:100', 'priority' => 'nullable|integer|min:1|max:100',
]); ]);
if (!$channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) { if (! $channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) {
return $this->sendError('Platform account is not attached to this channel.', [], 422); return $this->sendError('Platform account is not attached to this channel.', [], 422);
} }
@ -218,7 +219,7 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount
} catch (ValidationException $e) { } catch (ValidationException $e) {
return $this->sendValidationError($e->errors()); return $this->sendValidationError($e->errors());
} catch (Exception $e) { } catch (Exception $e) {
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500); return $this->sendError('Failed to update platform account relationship: '.$e->getMessage(), [], 500);
} }
} }
} }

View file

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified; use Illuminate\Auth\Events\Verified;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@ -19,7 +20,9 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse
} }
if ($request->user()->markEmailAsVerified()) { if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user())); /** @var MustVerifyEmail $user */
$user = $request->user();
event(new Verified($user));
} }
return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); return redirect()->intended(route('dashboard', absolute: false).'?verified=1');

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\ArticlePublication
*/
class ArticlePublicationResource extends JsonResource class ArticlePublicationResource extends JsonResource
{ {
/** /**
@ -17,10 +20,10 @@ public function toArray(Request $request): array
return [ return [
'id' => $this->id, 'id' => $this->id,
'article_id' => $this->article_id, 'article_id' => $this->article_id,
'status' => $this->status, 'platform' => $this->platform,
'published_at' => $this->published_at?->toISOString(), 'published_at' => $this->published_at->toISOString(),
'created_at' => $this->created_at->toISOString(), 'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(),
]; ];
} }
} }

View file

@ -6,10 +6,13 @@
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/** /**
* @property int $id * @mixin \App\Models\Article
*/ */
class ArticleResource extends JsonResource class ArticleResource extends JsonResource
{ {
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
return [ return [
@ -19,12 +22,8 @@ public function toArray(Request $request): array
'title' => $this->title, 'title' => $this->title,
'description' => $this->description, 'description' => $this->description,
'is_valid' => $this->is_valid, 'is_valid' => $this->is_valid,
'is_duplicate' => $this->is_duplicate,
'approval_status' => $this->approval_status, 'approval_status' => $this->approval_status,
'publish_status' => $this->publish_status, 'publish_status' => $this->publish_status,
'approved_at' => $this->approved_at?->toISOString(),
'approved_by' => $this->approved_by,
'fetched_at' => $this->fetched_at?->toISOString(),
'validated_at' => $this->validated_at?->toISOString(), 'validated_at' => $this->validated_at?->toISOString(),
'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null, 'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null,
'created_at' => $this->created_at->toISOString(), 'created_at' => $this->created_at->toISOString(),

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\Feed
*/
class FeedResource extends JsonResource class FeedResource extends JsonResource
{ {
/** /**
@ -26,9 +29,9 @@ public function toArray(Request $request): array
'created_at' => $this->created_at->toISOString(), 'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(),
'articles_count' => $this->when( 'articles_count' => $this->when(
$request->routeIs('api.feeds.*') && isset($this->articles_count), $request->routeIs('api.feeds.*') && isset($this->articles_count),
$this->articles_count $this->articles_count
), ),
]; ];
} }
} }

View file

@ -5,6 +5,12 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\PlatformAccount
*/
/**
* @mixin \App\Models\PlatformAccount
*/
class PlatformAccountResource extends JsonResource class PlatformAccountResource extends JsonResource
{ {
/** /**
@ -28,4 +34,4 @@ public function toArray(Request $request): array
'channels' => PlatformChannelResource::collection($this->whenLoaded('channels')), 'channels' => PlatformChannelResource::collection($this->whenLoaded('channels')),
]; ];
} }
} }

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\PlatformChannel
*/
class PlatformChannelResource extends JsonResource class PlatformChannelResource extends JsonResource
{ {
/** /**
@ -30,4 +33,4 @@ public function toArray(Request $request): array
'routes' => RouteResource::collection($this->whenLoaded('routes')), 'routes' => RouteResource::collection($this->whenLoaded('routes')),
]; ];
} }
} }

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\PlatformInstance
*/
class PlatformInstanceResource extends JsonResource class PlatformInstanceResource extends JsonResource
{ {
/** /**
@ -24,4 +27,4 @@ public function toArray(Request $request): array
'updated_at' => $this->updated_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(),
]; ];
} }
} }

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\Route
*/
class RouteResource extends JsonResource class RouteResource extends JsonResource
{ {
/** /**
@ -15,7 +18,6 @@ class RouteResource extends JsonResource
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
return [ return [
'id' => $this->id,
'feed_id' => $this->feed_id, 'feed_id' => $this->feed_id,
'platform_channel_id' => $this->platform_channel_id, 'platform_channel_id' => $this->platform_channel_id,
'is_active' => $this->is_active, 'is_active' => $this->is_active,
@ -35,4 +37,4 @@ public function toArray(Request $request): array
}), }),
]; ];
} }
} }

View file

@ -2,9 +2,9 @@
namespace App\Livewire; namespace App\Livewire;
use App\Jobs\ArticleDiscoveryJob;
use App\Models\Article; use App\Models\Article;
use App\Models\Setting; use App\Models\Setting;
use App\Jobs\ArticleDiscoveryJob;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
@ -39,7 +39,7 @@ public function refresh(): void
$this->dispatch('refresh-started'); $this->dispatch('refresh-started');
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$articles = Article::with(['feed', 'articlePublication']) $articles = Article::with(['feed', 'articlePublication'])
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')

View file

@ -13,7 +13,7 @@ class Channels extends Component
public function toggle(int $channelId): void public function toggle(int $channelId): void
{ {
$channel = PlatformChannel::findOrFail($channelId); $channel = PlatformChannel::findOrFail($channelId);
$channel->is_active = !$channel->is_active; $channel->is_active = ! $channel->is_active;
$channel->save(); $channel->save();
} }
@ -29,13 +29,13 @@ public function closeAccountModal(): void
public function attachAccount(int $accountId): void public function attachAccount(int $accountId): void
{ {
if (!$this->managingChannelId) { if (! $this->managingChannelId) {
return; return;
} }
$channel = PlatformChannel::findOrFail($this->managingChannelId); $channel = PlatformChannel::findOrFail($this->managingChannelId);
if (!$channel->platformAccounts()->where('platform_account_id', $accountId)->exists()) { if (! $channel->platformAccounts()->where('platform_account_id', $accountId)->exists()) {
$channel->platformAccounts()->attach($accountId, [ $channel->platformAccounts()->attach($accountId, [
'is_active' => true, 'is_active' => true,
'priority' => 1, 'priority' => 1,
@ -51,7 +51,7 @@ public function detachAccount(int $channelId, int $accountId): void
$channel->platformAccounts()->detach($accountId); $channel->platformAccounts()->detach($accountId);
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get(); $channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get();
$allAccounts = PlatformAccount::where('is_active', true)->get(); $allAccounts = PlatformAccount::where('is_active', true)->get();
@ -61,7 +61,7 @@ public function render()
: null; : null;
$availableAccounts = $managingChannel $availableAccounts = $managingChannel
? $allAccounts->filter(fn($account) => !$managingChannel->platformAccounts->contains('id', $account->id)) ? $allAccounts->filter(fn ($account) => ! $managingChannel->platformAccounts->contains('id', $account->id))
: collect(); : collect();
return view('livewire.channels', [ return view('livewire.channels', [

View file

@ -19,7 +19,7 @@ public function setPeriod(string $period): void
$this->period = $period; $this->period = $period;
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$service = app(DashboardStatsService::class); $service = app(DashboardStatsService::class);

View file

@ -10,11 +10,11 @@ class Feeds extends Component
public function toggle(int $feedId): void public function toggle(int $feedId): void
{ {
$feed = Feed::findOrFail($feedId); $feed = Feed::findOrFail($feedId);
$feed->is_active = !$feed->is_active; $feed->is_active = ! $feed->is_active;
$feed->save(); $feed->save();
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$feeds = Feed::orderBy('name')->get(); $feeds = Feed::orderBy('name')->get();

View file

@ -29,36 +29,54 @@ class Onboarding extends Component
// Platform form // Platform form
public string $instanceUrl = ''; public string $instanceUrl = '';
public string $username = ''; public string $username = '';
public string $password = ''; public string $password = '';
/** @var array<string, mixed>|null */
public ?array $existingAccount = null; public ?array $existingAccount = null;
// Feed form // Feed form
public string $feedName = ''; public string $feedName = '';
public string $feedProvider = 'vrt'; public string $feedProvider = 'vrt';
public ?int $feedLanguageId = null; public ?int $feedLanguageId = null;
public string $feedDescription = ''; public string $feedDescription = '';
// Channel form // Channel form
public string $channelName = ''; public string $channelName = '';
public ?int $platformInstanceId = null; public ?int $platformInstanceId = null;
public ?int $channelLanguageId = null; public ?int $channelLanguageId = null;
public string $channelDescription = ''; public string $channelDescription = '';
// Route form // Route form
public ?int $routeFeedId = null; public ?int $routeFeedId = null;
public ?int $routeChannelId = null; public ?int $routeChannelId = null;
public int $routePriority = 50; public int $routePriority = 50;
// State // State
/** @var array<string, string> */
public array $formErrors = []; public array $formErrors = [];
public bool $isLoading = false; public bool $isLoading = false;
#[\Livewire\Attributes\Locked] #[\Livewire\Attributes\Locked]
public ?int $previousChannelLanguageId = null; public ?int $previousChannelLanguageId = null;
protected CreatePlatformAccountAction $createPlatformAccountAction; protected CreatePlatformAccountAction $createPlatformAccountAction;
protected CreateFeedAction $createFeedAction; protected CreateFeedAction $createFeedAction;
protected CreateChannelAction $createChannelAction; protected CreateChannelAction $createChannelAction;
protected CreateRouteAction $createRouteAction; protected CreateRouteAction $createRouteAction;
public function boot( public function boot(
@ -188,7 +206,7 @@ public function createPlatformAccount(): void
} }
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Lemmy platform account creation failed', [ logger()->error('Lemmy platform account creation failed', [
'instance_url' => 'https://' . $this->instanceUrl, 'instance_url' => 'https://'.$this->instanceUrl,
'username' => $this->username, 'username' => $this->username,
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'class' => get_class($e), 'class' => get_class($e),
@ -319,17 +337,20 @@ public function completeOnboarding(): void
/** /**
* Get language codes that have at least one active provider. * Get language codes that have at least one active provider.
*/ */
/**
* @return list<string>
*/
public function getAvailableLanguageCodes(): array public function getAvailableLanguageCodes(): array
{ {
$providers = config('feed.providers', []); $providers = config('feed.providers', []);
$languageCodes = []; $languageCodes = [];
foreach ($providers as $provider) { foreach ($providers as $provider) {
if (!($provider['is_active'] ?? false)) { if (! ($provider['is_active'] ?? false)) {
continue; continue;
} }
foreach (array_keys($provider['languages'] ?? []) as $code) { foreach (array_keys($provider['languages'] ?? []) as $code) {
$languageCodes[$code] = true; $languageCodes[(string) $code] = true;
} }
} }
@ -339,14 +360,17 @@ public function getAvailableLanguageCodes(): array
/** /**
* Get providers available for the current channel language. * Get providers available for the current channel language.
*/ */
/**
* @return array<int, array<string, string>>
*/
public function getProvidersForLanguage(): array public function getProvidersForLanguage(): array
{ {
if (!$this->channelLanguageId) { if (! $this->channelLanguageId) {
return []; return [];
} }
$language = Language::find($this->channelLanguageId); $language = Language::find($this->channelLanguageId);
if (!$language) { if (! $language) {
return []; return [];
} }
@ -355,7 +379,7 @@ public function getProvidersForLanguage(): array
$available = []; $available = [];
foreach ($providers as $key => $provider) { foreach ($providers as $key => $provider) {
if (!($provider['is_active'] ?? false)) { if (! ($provider['is_active'] ?? false)) {
continue; continue;
} }
if (isset($provider['languages'][$langCode])) { if (isset($provider['languages'][$langCode])) {
@ -375,13 +399,14 @@ public function getProvidersForLanguage(): array
*/ */
public function getChannelLanguage(): ?Language public function getChannelLanguage(): ?Language
{ {
if (!$this->channelLanguageId) { if (! $this->channelLanguageId) {
return null; return null;
} }
return Language::find($this->channelLanguageId); return Language::find($this->channelLanguageId);
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
// For channel step: only show languages that have providers // For channel step: only show languages that have providers
$availableCodes = $this->getAvailableLanguageCodes(); $availableCodes = $this->getAvailableLanguageCodes();

View file

@ -11,12 +11,16 @@
class Routes extends Component class Routes extends Component
{ {
public bool $showCreateModal = false; public bool $showCreateModal = false;
public ?int $editingFeedId = null; public ?int $editingFeedId = null;
public ?int $editingChannelId = null; public ?int $editingChannelId = null;
// Create form // Create form
public ?int $newFeedId = null; public ?int $newFeedId = null;
public ?int $newChannelId = null; public ?int $newChannelId = null;
public int $newPriority = 50; public int $newPriority = 50;
// Edit form // Edit form
@ -24,6 +28,7 @@ class Routes extends Component
// Keyword management // Keyword management
public string $newKeyword = ''; public string $newKeyword = '';
public bool $showKeywordInput = false; public bool $showKeywordInput = false;
public function openCreateModal(): void public function openCreateModal(): void
@ -53,6 +58,7 @@ public function createRoute(): void
if ($exists) { if ($exists) {
$this->addError('newFeedId', 'This route already exists.'); $this->addError('newFeedId', 'This route already exists.');
return; return;
} }
@ -87,7 +93,7 @@ public function closeEditModal(): void
public function updateRoute(): void public function updateRoute(): void
{ {
if (!$this->editingFeedId || !$this->editingChannelId) { if (! $this->editingFeedId || ! $this->editingChannelId) {
return; return;
} }
@ -108,7 +114,7 @@ public function toggle(int $feedId, int $channelId): void
->where('platform_channel_id', $channelId) ->where('platform_channel_id', $channelId)
->firstOrFail(); ->firstOrFail();
$route->is_active = !$route->is_active; $route->is_active = ! $route->is_active;
$route->save(); $route->save();
} }
@ -126,7 +132,7 @@ public function delete(int $feedId, int $channelId): void
public function addKeyword(): void public function addKeyword(): void
{ {
if (!$this->editingFeedId || !$this->editingChannelId || empty(trim($this->newKeyword))) { if (! $this->editingFeedId || ! $this->editingChannelId || empty(trim($this->newKeyword))) {
return; return;
} }
@ -144,7 +150,7 @@ public function addKeyword(): void
public function toggleKeyword(int $keywordId): void public function toggleKeyword(int $keywordId): void
{ {
$keyword = Keyword::findOrFail($keywordId); $keyword = Keyword::findOrFail($keywordId);
$keyword->is_active = !$keyword->is_active; $keyword->is_active = ! $keyword->is_active;
$keyword->save(); $keyword->save();
} }
@ -153,22 +159,23 @@ public function deleteKeyword(int $keywordId): void
Keyword::destroy($keywordId); Keyword::destroy($keywordId);
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$routes = Route::with(['feed', 'platformChannel']) $routes = Route::with(['feed', 'platformChannel'])
->orderBy('priority', 'desc') ->orderBy('priority', 'desc')
->get(); ->get();
// Batch load keywords for all routes to avoid N+1 queries // Batch load keywords for all routes to avoid N+1 queries
$routeKeys = $routes->map(fn($r) => $r->feed_id . '-' . $r->platform_channel_id); $routeKeys = $routes->map(fn ($r) => $r->feed_id.'-'.$r->platform_channel_id);
$allKeywords = Keyword::whereIn('feed_id', $routes->pluck('feed_id')) $allKeywords = Keyword::whereIn('feed_id', $routes->pluck('feed_id'))
->whereIn('platform_channel_id', $routes->pluck('platform_channel_id')) ->whereIn('platform_channel_id', $routes->pluck('platform_channel_id'))
->get() ->get()
->groupBy(fn($k) => $k->feed_id . '-' . $k->platform_channel_id); ->groupBy(fn ($k) => $k->feed_id.'-'.$k->platform_channel_id);
$routes = $routes->map(function ($route) use ($allKeywords) { $routes = $routes->map(function ($route) use ($allKeywords) {
$key = $route->feed_id . '-' . $route->platform_channel_id; $key = $route->feed_id.'-'.$route->platform_channel_id;
$route->keywords = $allKeywords->get($key, collect()); $route->setRelation('keywords', $allKeywords->get($key, collect()));
return $route; return $route;
}); });

View file

@ -8,10 +8,13 @@
class Settings extends Component class Settings extends Component
{ {
public bool $articleProcessingEnabled = true; public bool $articleProcessingEnabled = true;
public bool $publishingApprovalsEnabled = false; public bool $publishingApprovalsEnabled = false;
public int $articlePublishingInterval = 5; public int $articlePublishingInterval = 5;
public ?string $successMessage = null; public ?string $successMessage = null;
public ?string $errorMessage = null; public ?string $errorMessage = null;
public function mount(): void public function mount(): void
@ -23,14 +26,14 @@ public function mount(): void
public function toggleArticleProcessing(): void public function toggleArticleProcessing(): void
{ {
$this->articleProcessingEnabled = !$this->articleProcessingEnabled; $this->articleProcessingEnabled = ! $this->articleProcessingEnabled;
Setting::setArticleProcessingEnabled($this->articleProcessingEnabled); Setting::setArticleProcessingEnabled($this->articleProcessingEnabled);
$this->showSuccess(); $this->showSuccess();
} }
public function togglePublishingApprovals(): void public function togglePublishingApprovals(): void
{ {
$this->publishingApprovalsEnabled = !$this->publishingApprovalsEnabled; $this->publishingApprovalsEnabled = ! $this->publishingApprovalsEnabled;
Setting::setPublishingApprovalsEnabled($this->publishingApprovalsEnabled); Setting::setPublishingApprovalsEnabled($this->publishingApprovalsEnabled);
$this->showSuccess(); $this->showSuccess();
} }
@ -60,7 +63,7 @@ public function clearMessages(): void
$this->errorMessage = null; $this->errorMessage = null;
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
return view('livewire.settings')->layout('layouts.app'); return view('livewire.settings')->layout('layouts.app');
} }

View file

@ -15,15 +15,20 @@
* @method static firstOrCreate(array<string, mixed> $array) * @method static firstOrCreate(array<string, mixed> $array)
* @method static where(string $string, string $url) * @method static where(string $string, string $url)
* @method static create(array<string, mixed> $array) * @method static create(array<string, mixed> $array)
* @property integer $id *
* @property int $id
* @property int $feed_id * @property int $feed_id
* @property Feed $feed * @property Feed $feed
* @property string $url * @property string $url
* @property string $title
* @property string|null $description
* @property string $approval_status
* @property string $publish_status
* @property bool|null $is_valid * @property bool|null $is_valid
* @property Carbon|null $validated_at * @property Carbon|null $validated_at
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at * @property Carbon $updated_at
* @property ArticlePublication $articlePublication * @property ArticlePublication|null $articlePublication
*/ */
class Article extends Model class Article extends Model
{ {
@ -79,7 +84,7 @@ public function isRejected(): bool
return $this->approval_status === 'rejected'; return $this->approval_status === 'rejected';
} }
public function approve(string $approvedBy = null): void public function approve(?string $approvedBy = null): void
{ {
$this->update([ $this->update([
'approval_status' => 'approved', 'approval_status' => 'approved',
@ -89,7 +94,7 @@ public function approve(string $approvedBy = null): void
event(new ArticleApproved($this)); event(new ArticleApproved($this));
} }
public function reject(string $rejectedBy = null): void public function reject(?string $rejectedBy = null): void
{ {
$this->update([ $this->update([
'approval_status' => 'rejected', 'approval_status' => 'rejected',
@ -98,12 +103,12 @@ public function reject(string $rejectedBy = null): void
public function canBePublished(): bool public function canBePublished(): bool
{ {
if (!$this->isValid()) { if (! $this->isValid()) {
return false; return false;
} }
// If approval system is disabled, auto-approve valid articles // If approval system is disabled, auto-approve valid articles
if (!\App\Models\Setting::isPublishingApprovalsEnabled()) { if (! \App\Models\Setting::isPublishingApprovalsEnabled()) {
return true; return true;
} }

View file

@ -8,9 +8,16 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property integer $article_id * @property int $id
* @property integer $platform_channel_id * @property int $article_id
* @property integer $post_id * @property int $platform_channel_id
* @property string $post_id
* @property string $platform
* @property string $published_by
* @property array<string, mixed>|null $publication_data
* @property \Illuminate\Support\Carbon $published_at
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* *
* @method static create(array<string, mixed> $array) * @method static create(array<string, mixed> $array)
*/ */
@ -18,7 +25,7 @@ class ArticlePublication extends Model
{ {
/** @use HasFactory<ArticlePublicationFactory> */ /** @use HasFactory<ArticlePublicationFactory> */
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'article_id', 'article_id',
'platform_channel_id', 'platform_channel_id',

View file

@ -24,6 +24,7 @@
* @property Carbon|null $last_fetched_at * @property Carbon|null $last_fetched_at
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at * @property Carbon $updated_at
*
* @method static orderBy(string $string, string $string1) * @method static orderBy(string $string, string $string1)
* @method static where(string $string, true $true) * @method static where(string $string, true $true)
* @method static findOrFail(mixed $feed_id) * @method static findOrFail(mixed $feed_id)
@ -32,7 +33,9 @@ class Feed extends Model
{ {
/** @use HasFactory<FeedFactory> */ /** @use HasFactory<FeedFactory> */
use HasFactory; use HasFactory;
private const RECENT_FETCH_THRESHOLD_HOURS = 2; private const RECENT_FETCH_THRESHOLD_HOURS = 2;
private const DAILY_FETCH_THRESHOLD_HOURS = 24; private const DAILY_FETCH_THRESHOLD_HOURS = 24;
protected $fillable = [ protected $fillable = [
@ -44,13 +47,13 @@ class Feed extends Model
'description', 'description',
'settings', 'settings',
'is_active', 'is_active',
'last_fetched_at' 'last_fetched_at',
]; ];
protected $casts = [ protected $casts = [
'settings' => 'array', 'settings' => 'array',
'is_active' => 'boolean', 'is_active' => 'boolean',
'last_fetched_at' => 'datetime' 'last_fetched_at' => 'datetime',
]; ];
public function getTypeDisplayAttribute(): string public function getTypeDisplayAttribute(): string
@ -64,11 +67,11 @@ public function getTypeDisplayAttribute(): string
public function getStatusAttribute(): string public function getStatusAttribute(): string
{ {
if (!$this->is_active) { if (! $this->is_active) {
return 'Inactive'; return 'Inactive';
} }
if (!$this->last_fetched_at) { if (! $this->last_fetched_at) {
return 'Never fetched'; return 'Never fetched';
} }
@ -79,12 +82,12 @@ public function getStatusAttribute(): string
} elseif ($hoursAgo < self::DAILY_FETCH_THRESHOLD_HOURS) { } elseif ($hoursAgo < self::DAILY_FETCH_THRESHOLD_HOURS) {
return "Fetched {$hoursAgo}h ago"; return "Fetched {$hoursAgo}h ago";
} else { } else {
return "Fetched " . $this->last_fetched_at->diffForHumans(); return 'Fetched '.$this->last_fetched_at->diffForHumans();
} }
} }
/** /**
* @return BelongsToMany<PlatformChannel, $this, Route> * @return BelongsToMany<PlatformChannel, $this>
*/ */
public function channels(): BelongsToMany public function channels(): BelongsToMany
{ {
@ -94,7 +97,7 @@ public function channels(): BelongsToMany
} }
/** /**
* @return BelongsToMany<PlatformChannel, $this, Route> * @return BelongsToMany<PlatformChannel, $this>
*/ */
public function activeChannels(): BelongsToMany public function activeChannels(): BelongsToMany
{ {

View file

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Database\Factories\KeywordFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -20,17 +21,18 @@
*/ */
class Keyword extends Model class Keyword extends Model
{ {
/** @use HasFactory<KeywordFactory> */
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'feed_id', 'feed_id',
'platform_channel_id', 'platform_channel_id',
'keyword', 'keyword',
'is_active' 'is_active',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean' 'is_active' => 'boolean',
]; ];
/** /**
@ -48,5 +50,4 @@ public function platformChannel(): BelongsTo
{ {
return $this->belongsTo(PlatformChannel::class); return $this->belongsTo(PlatformChannel::class);
} }
} }

View file

@ -15,13 +15,13 @@ class Language extends Model
protected $fillable = [ protected $fillable = [
'short_code', 'short_code',
'name', 'name',
'native_name', 'native_name',
'is_active' 'is_active',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean' 'is_active' => 'boolean',
]; ];
/** /**

View file

@ -3,12 +3,14 @@
namespace App\Models; namespace App\Models;
use App\Enums\LogLevelEnum; use App\Enums\LogLevelEnum;
use Database\Factories\LogFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
/** /**
* @method static create(array $array) * @method static create(array<string, mixed> $array)
*
* @property LogLevelEnum $level * @property LogLevelEnum $level
* @property string $message * @property string $message
* @property array<string, mixed> $context * @property array<string, mixed> $context
@ -17,6 +19,7 @@
*/ */
class Log extends Model class Log extends Model
{ {
/** @use HasFactory<LogFactory> */
use HasFactory; use HasFactory;
protected $table = 'logs'; protected $table = 'logs';

View file

@ -2,15 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Enums\PlatformEnum;
use Database\Factories\PlatformAccountFactory; use Database\Factories\PlatformAccountFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use App\Enums\PlatformEnum;
/** /**
* @property int $id * @property int $id
@ -18,13 +18,14 @@
* @property string $instance_url * @property string $instance_url
* @property string $username * @property string $username
* @property string $password * @property string $password
* @property string $settings * @property array<string, mixed> $settings
* @property bool $is_active * @property bool $is_active
* @property Carbon $last_tested_at * @property Carbon|null $last_tested_at
* @property string $status * @property string $status
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at * @property Carbon $updated_at
* @property Collection<int, PlatformChannel> $activeChannels * @property Collection<int, PlatformChannel> $activeChannels
*
* @method static where(string $string, PlatformEnum $platform) * @method static where(string $string, PlatformEnum $platform)
* @method static orderBy(string $string) * @method static orderBy(string $string)
* @method static create(array<string, mixed> $validated) * @method static create(array<string, mixed> $validated)
@ -42,14 +43,14 @@ class PlatformAccount extends Model
'settings', 'settings',
'is_active', 'is_active',
'last_tested_at', 'last_tested_at',
'status' 'status',
]; ];
protected $casts = [ protected $casts = [
'platform' => PlatformEnum::class, 'platform' => PlatformEnum::class,
'settings' => 'array', 'settings' => 'array',
'is_active' => 'boolean', 'is_active' => 'boolean',
'last_tested_at' => 'datetime' 'last_tested_at' => 'datetime',
]; ];
// Encrypt password when storing // Encrypt password when storing
@ -64,12 +65,12 @@ protected function password(): Attribute
if (is_null($value)) { if (is_null($value)) {
return null; return null;
} }
// Return empty string if value is empty // Return empty string if value is empty
if (empty($value)) { if (empty($value)) {
return ''; return '';
} }
try { try {
return Crypt::decryptString($value); return Crypt::decryptString($value);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -82,18 +83,17 @@ protected function password(): Attribute
if (is_null($value)) { if (is_null($value)) {
return null; return null;
} }
// Store empty string as null // Store empty string as null
if (empty($value)) { if (empty($value)) {
return null; return null;
} }
return Crypt::encryptString($value); return Crypt::encryptString($value);
}, },
)->withoutObjectCaching(); )->withoutObjectCaching();
} }
// Get the active accounts for a platform (returns collection) // Get the active accounts for a platform (returns collection)
/** /**
* @return Collection<int, PlatformAccount> * @return Collection<int, PlatformAccount>

View file

@ -10,15 +10,16 @@
/** /**
* @method static findMany(mixed $channel_ids) * @method static findMany(mixed $channel_ids)
* @method static create(array $array) * @method static create(array<string, mixed> $array)
* @property integer $id *
* @property integer $platform_instance_id * @property int $id
* @property int $platform_instance_id
* @property PlatformInstance $platformInstance * @property PlatformInstance $platformInstance
* @property integer $channel_id * @property int $channel_id
* @property string $name * @property string $name
* @property int $language_id * @property int $language_id
* @property Language|null $language * @property Language|null $language
* @property boolean $is_active * @property bool $is_active
*/ */
class PlatformChannel extends Model class PlatformChannel extends Model
{ {
@ -34,11 +35,11 @@ class PlatformChannel extends Model
'channel_id', 'channel_id',
'description', 'description',
'language_id', 'language_id',
'is_active' 'is_active',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean' 'is_active' => 'boolean',
]; ];
/** /**
@ -70,11 +71,11 @@ public function activePlatformAccounts(): BelongsToMany
public function getFullNameAttribute(): string public function getFullNameAttribute(): string
{ {
// For Lemmy, use /c/ prefix // For Lemmy, use /c/ prefix
return $this->platformInstance->url . '/c/' . $this->name; return $this->platformInstance->url.'/c/'.$this->name;
} }
/** /**
* @return BelongsToMany<Feed, $this, Route> * @return BelongsToMany<Feed, $this>
*/ */
public function feeds(): BelongsToMany public function feeds(): BelongsToMany
{ {
@ -84,7 +85,7 @@ public function feeds(): BelongsToMany
} }
/** /**
* @return BelongsToMany<Feed, $this, Route> * @return BelongsToMany<Feed, $this>
*/ */
public function activeFeeds(): BelongsToMany public function activeFeeds(): BelongsToMany
{ {

View file

@ -12,7 +12,9 @@
*/ */
class PlatformChannelPost extends Model class PlatformChannelPost extends Model
{ {
/** @use HasFactory<\Illuminate\Database\Eloquent\Factories\Factory<PlatformChannelPost>> */
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'platform', 'platform',
'channel_id', 'channel_id',
@ -44,7 +46,7 @@ public static function urlExists(PlatformEnum $platform, string $channelId, stri
public static function duplicateExists(PlatformEnum $platform, string $channelId, ?string $url, ?string $title): bool public static function duplicateExists(PlatformEnum $platform, string $channelId, ?string $url, ?string $title): bool
{ {
if (!$url && !$title) { if (! $url && ! $title) {
return false; return false;
} }

View file

@ -12,28 +12,29 @@
/** /**
* @method static updateOrCreate(array<string, mixed> $array, $instanceData) * @method static updateOrCreate(array<string, mixed> $array, $instanceData)
* @method static where(string $string, mixed $operator) * @method static where(string $string, mixed $operator)
*
* @property PlatformEnum $platform * @property PlatformEnum $platform
* @property string $url * @property string $url
* @property string $name * @property string $name
* @property string $description * @property string $description
* @property boolean $is_active * @property bool $is_active
*/ */
class PlatformInstance extends Model class PlatformInstance extends Model
{ {
/** @use HasFactory<PlatformInstanceFactory> */ /** @use HasFactory<PlatformInstanceFactory> */
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'platform', 'platform',
'url', 'url',
'name', 'name',
'description', 'description',
'is_active' 'is_active',
]; ];
protected $casts = [ protected $casts = [
'platform' => PlatformEnum::class, 'platform' => PlatformEnum::class,
'is_active' => 'boolean' 'is_active' => 'boolean',
]; ];
/** /**

View file

@ -4,9 +4,9 @@
use Database\Factories\RouteFactory; use Database\Factories\RouteFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
/** /**
@ -21,22 +21,23 @@ class Route extends Model
{ {
/** @use HasFactory<RouteFactory> */ /** @use HasFactory<RouteFactory> */
use HasFactory; use HasFactory;
protected $table = 'routes'; protected $table = 'routes';
// Laravel doesn't handle composite primary keys well, so we'll use regular queries // Laravel doesn't handle composite primary keys well, so we'll use regular queries
protected $primaryKey = null; protected $primaryKey = null;
public $incrementing = false; public $incrementing = false;
protected $fillable = [ protected $fillable = [
'feed_id', 'feed_id',
'platform_channel_id', 'platform_channel_id',
'is_active', 'is_active',
'priority' 'priority',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean' 'is_active' => 'boolean',
]; ];
/** /**

View file

@ -2,16 +2,18 @@
namespace App\Models; namespace App\Models;
use Database\Factories\SettingFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/** /**
* @method static updateOrCreate(string[] $array, array $array1) * @method static updateOrCreate(array<string, string> $array, array<string, mixed> $array1)
* @method static create(string[] $array) * @method static create(array<string, string> $array)
* @method static where(string $string, string $key) * @method static where(string $string, string $key)
*/ */
class Setting extends Model class Setting extends Model
{ {
/** @use HasFactory<SettingFactory> */
use HasFactory; use HasFactory;
protected $fillable = ['key', 'value']; protected $fillable = ['key', 'value'];

View file

@ -11,7 +11,7 @@
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */ /** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasApiTokens; use HasApiTokens, HasFactory, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View file

@ -2,13 +2,15 @@
namespace App\Modules\Lemmy; namespace App\Modules\Lemmy;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response; use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
class LemmyRequest class LemmyRequest
{ {
private string $instance; private string $instance;
private ?string $token; private ?string $token;
private string $scheme = 'https'; private string $scheme = 'https';
public function __construct(string $instance, ?string $token = null) public function __construct(string $instance, ?string $token = null)
@ -45,11 +47,12 @@ public function withScheme(string $scheme): self
if (in_array($scheme, ['http', 'https'], true)) { if (in_array($scheme, ['http', 'https'], true)) {
$this->scheme = $scheme; $this->scheme = $scheme;
} }
return $this; return $this;
} }
/** /**
* @param array<string, mixed> $params * @param array<string, mixed> $params
*/ */
public function get(string $endpoint, array $params = []): Response public function get(string $endpoint, array $params = []): Response
{ {
@ -65,7 +68,7 @@ public function get(string $endpoint, array $params = []): Response
} }
/** /**
* @param array<string, mixed> $data * @param array<string, mixed> $data
*/ */
public function post(string $endpoint, array $data = []): Response public function post(string $endpoint, array $data = []): Response
{ {
@ -83,6 +86,7 @@ public function post(string $endpoint, array $data = []): Response
public function withToken(string $token): self public function withToken(string $token): self
{ {
$this->token = $token; $this->token = $token;
return $this; return $this;
} }
} }

View file

@ -39,7 +39,7 @@ public function login(string $username, string $password): ?string
'password' => $password, 'password' => $password,
]); ]);
if (!$response->successful()) { if (! $response->successful()) {
$responseBody = $response->body(); $responseBody = $response->body();
logger()->error('Lemmy login failed', [ logger()->error('Lemmy login failed', [
'status' => $response->status(), 'status' => $response->status(),
@ -61,6 +61,7 @@ public function login(string $username, string $password): ?string
} }
$data = $response->json(); $data = $response->json();
return $data['jwt'] ?? null; return $data['jwt'] ?? null;
} catch (Exception $e) { } catch (Exception $e) {
// Re-throw rate limit exceptions immediately // Re-throw rate limit exceptions immediately
@ -74,7 +75,7 @@ public function login(string $username, string $password): ?string
continue; continue;
} }
// Connection failed - throw exception to distinguish from auth failure // Connection failed - throw exception to distinguish from auth failure
throw new Exception('Connection failed: ' . $e->getMessage()); throw new Exception('Connection failed: '.$e->getMessage());
} }
} }
@ -88,11 +89,12 @@ public function getCommunityId(string $communityName, string $token): int
$request = new LemmyRequest($this->instance, $token); $request = new LemmyRequest($this->instance, $token);
$response = $request->get('community', ['name' => $communityName]); $response = $request->get('community', ['name' => $communityName]);
if (!$response->successful()) { if (! $response->successful()) {
throw new Exception('Failed to fetch community: ' . $response->status()); throw new Exception('Failed to fetch community: '.$response->status());
} }
$data = $response->json(); $data = $response->json();
return $data['community_view']['community']['id'] ?? throw new Exception('Community not found'); return $data['community_view']['community']['id'] ?? throw new Exception('Community not found');
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Community lookup failed', ['error' => $e->getMessage()]); logger()->error('Community lookup failed', ['error' => $e->getMessage()]);
@ -107,14 +109,15 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
$response = $request->get('post/list', [ $response = $request->get('post/list', [
'community_id' => $platformChannelId, 'community_id' => $platformChannelId,
'limit' => 50, 'limit' => 50,
'sort' => 'New' 'sort' => 'New',
]); ]);
if (!$response->successful()) { if (! $response->successful()) {
logger()->warning('Failed to sync channel posts', [ logger()->warning('Failed to sync channel posts', [
'status' => $response->status(), 'status' => $response->status(),
'platform_channel_id' => $platformChannelId 'platform_channel_id' => $platformChannelId,
]); ]);
return; return;
} }
@ -137,13 +140,13 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
logger()->info('Synced channel posts', [ logger()->info('Synced channel posts', [
'platform_channel_id' => $platformChannelId, 'platform_channel_id' => $platformChannelId,
'posts_count' => count($posts) 'posts_count' => count($posts),
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Exception while syncing channel posts', [ logger()->error('Exception while syncing channel posts', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'platform_channel_id' => $platformChannelId 'platform_channel_id' => $platformChannelId,
]); ]);
} }
} }
@ -176,8 +179,8 @@ public function createPost(string $token, string $title, string $body, int $plat
$response = $request->post('post', $postData); $response = $request->post('post', $postData);
if (!$response->successful()) { if (! $response->successful()) {
throw new Exception('Failed to create post: ' . $response->status() . ' - ' . $response->body()); throw new Exception('Failed to create post: '.$response->status().' - '.$response->body());
} }
return $response->json(); return $response->json();
@ -196,19 +199,22 @@ public function getLanguages(): array
$request = new LemmyRequest($this->instance); $request = new LemmyRequest($this->instance);
$response = $request->get('site'); $response = $request->get('site');
if (!$response->successful()) { if (! $response->successful()) {
logger()->warning('Failed to fetch site languages', [ logger()->warning('Failed to fetch site languages', [
'status' => $response->status() 'status' => $response->status(),
]); ]);
return []; return [];
} }
$data = $response->json(); $data = $response->json();
return $data['all_languages'] ?? []; return $data['all_languages'] ?? [];
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Exception while fetching languages', [ logger()->error('Exception while fetching languages', [
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
return []; return [];
} }
} }

View file

@ -12,6 +12,7 @@
class LemmyPublisher class LemmyPublisher
{ {
private LemmyApiService $api; private LemmyApiService $api;
private PlatformAccount $account; private PlatformAccount $account;
public function __construct(PlatformAccount $account) public function __construct(PlatformAccount $account)
@ -21,8 +22,9 @@ public function __construct(PlatformAccount $account)
} }
/** /**
* @param array<string, mixed> $extractedData * @param array<string, mixed> $extractedData
* @return array<string, mixed> * @return array<string, mixed>
*
* @throws PlatformAuthException * @throws PlatformAuthException
* @throws Exception * @throws Exception
*/ */
@ -37,6 +39,7 @@ public function publishToChannel(Article $article, array $extractedData, Platfor
// If the cached token was stale, refresh and retry once // If the cached token was stale, refresh and retry once
if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) { if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
$token = $authService->refreshToken($this->account); $token = $authService->refreshToken($this->account);
return $this->createPost($token, $extractedData, $channel, $article); return $this->createPost($token, $extractedData, $channel, $article);
} }
throw $e; throw $e;
@ -44,7 +47,7 @@ public function publishToChannel(Article $article, array $extractedData, Platfor
} }
/** /**
* @param array<string, mixed> $extractedData * @param array<string, mixed> $extractedData
* @return array<string, mixed> * @return array<string, mixed>
*/ */
private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article): array private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article): array
@ -65,5 +68,4 @@ private function createPost(string $token, array $extractedData, PlatformChannel
$languageId $languageId
); );
} }
} }

View file

@ -4,9 +4,9 @@
use App\Models\Article; use App\Models\Article;
use App\Models\Feed; use App\Models\Feed;
use App\Services\Http\HttpFetcher;
use App\Services\Factories\ArticleParserFactory; use App\Services\Factories\ArticleParserFactory;
use App\Services\Factories\HomepageParserFactory; use App\Services\Factories\HomepageParserFactory;
use App\Services\Http\HttpFetcher;
use App\Services\Log\LogSaver; use App\Services\Log\LogSaver;
use Exception; use Exception;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -28,9 +28,9 @@ public function getArticlesFromFeed(Feed $feed): Collection
return $this->getArticlesFromWebsiteFeed($feed); return $this->getArticlesFromWebsiteFeed($feed);
} }
$this->logSaver->warning("Unsupported feed type", null, [ $this->logSaver->warning('Unsupported feed type', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_type' => $feed->type 'feed_type' => $feed->type,
]); ]);
return collect(); return collect();
@ -53,8 +53,8 @@ private function getArticlesFromRssFeed(Feed $feed): Collection
libxml_use_internal_errors($previousUseErrors); libxml_use_internal_errors($previousUseErrors);
} }
if ($rss === false || !isset($rss->channel->item)) { if ($rss === false || ! isset($rss->channel->item)) {
$this->logSaver->warning("Failed to parse RSS feed XML", null, [ $this->logSaver->warning('Failed to parse RSS feed XML', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_url' => $feed->url, 'feed_url' => $feed->url,
]); ]);
@ -72,7 +72,7 @@ private function getArticlesFromRssFeed(Feed $feed): Collection
return $articles; return $articles;
} catch (Exception $e) { } catch (Exception $e) {
$this->logSaver->error("Failed to fetch articles from RSS feed", null, [ $this->logSaver->error('Failed to fetch articles from RSS feed', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_url' => $feed->url, 'feed_url' => $feed->url,
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -92,9 +92,9 @@ private function getArticlesFromWebsiteFeed(Feed $feed): Collection
$parser = HomepageParserFactory::getParserForFeed($feed); $parser = HomepageParserFactory::getParserForFeed($feed);
if (! $parser) { if (! $parser) {
$this->logSaver->warning("No parser available for feed URL", null, [ $this->logSaver->warning('No parser available for feed URL', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_url' => $feed->url 'feed_url' => $feed->url,
]); ]);
return collect(); return collect();
@ -107,10 +107,10 @@ private function getArticlesFromWebsiteFeed(Feed $feed): Collection
->map(fn (string $url) => $this->saveArticle($url, $feed->id)); ->map(fn (string $url) => $this->saveArticle($url, $feed->id));
} catch (Exception $e) { } catch (Exception $e) {
$this->logSaver->error("Failed to fetch articles from website feed", null, [ $this->logSaver->error('Failed to fetch articles from website feed', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_url' => $feed->url, 'feed_url' => $feed->url,
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
return collect(); return collect();
@ -130,7 +130,7 @@ public function fetchArticleData(Article $article): array
} catch (Exception $e) { } catch (Exception $e) {
$this->logSaver->error('Exception while fetching article data', null, [ $this->logSaver->error('Exception while fetching article data', null, [
'url' => $article->url, 'url' => $article->url,
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
return []; return [];
@ -156,7 +156,7 @@ private function saveArticle(string $url, ?int $feedId = null): Article
return $article; return $article;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logSaver->error("Failed to create article", null, [ $this->logSaver->error('Failed to create article', null, [
'url' => $url, 'url' => $url,
'feed_id' => $feedId, 'feed_id' => $feedId,
'error' => $e->getMessage(), 'error' => $e->getMessage(),

View file

@ -12,23 +12,23 @@ public function __construct(
public function validate(Article $article): Article public function validate(Article $article): Article
{ {
logger('Checking keywords for article: ' . $article->id); logger('Checking keywords for article: '.$article->id);
$articleData = $this->articleFetcher->fetchArticleData($article); $articleData = $this->articleFetcher->fetchArticleData($article);
// Update article with fetched metadata (title, description) // Update article with fetched metadata (title, description)
$updateData = []; $updateData = [];
if (!empty($articleData)) { if (! empty($articleData)) {
$updateData['title'] = $articleData['title'] ?? $article->title; $updateData['title'] = $articleData['title'] ?? $article->title;
$updateData['description'] = $articleData['description'] ?? $article->description; $updateData['description'] = $articleData['description'] ?? $article->description;
$updateData['content'] = $articleData['full_article'] ?? null; $updateData['content'] = $articleData['full_article'] ?? null;
} }
if (!isset($articleData['full_article']) || empty($articleData['full_article'])) { if (! isset($articleData['full_article']) || empty($articleData['full_article'])) {
logger()->warning('Article data missing full_article content', [ logger()->warning('Article data missing full_article content', [
'article_id' => $article->id, 'article_id' => $article->id,
'url' => $article->url 'url' => $article->url,
]); ]);
$updateData['approval_status'] = 'rejected'; $updateData['approval_status'] = 'rejected';
@ -67,7 +67,7 @@ private function validateByKeywords(string $full_article): bool
// Common Belgian news topics // Common Belgian news topics
'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy', 'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy',
'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police' 'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police',
]; ];
foreach ($keywords as $keyword) { foreach ($keywords as $keyword) {

View file

@ -32,14 +32,14 @@ public function getToken(PlatformAccount $account): string
public function refreshToken(PlatformAccount $account): string public function refreshToken(PlatformAccount $account): string
{ {
if (! $account->username || ! $account->password || ! $account->instance_url) { if (! $account->username || ! $account->password || ! $account->instance_url) {
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials for account: ' . $account->username); throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials for account: '.$account->username);
} }
$api = new LemmyApiService($account->instance_url); $api = new LemmyApiService($account->instance_url);
$token = $api->login($account->username, $account->password); $token = $api->login($account->username, $account->password);
if (!$token) { if (! $token) {
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account: ' . $account->username); throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account: '.$account->username);
} }
// Cache the token for future use // Cache the token for future use
@ -52,15 +52,19 @@ public function refreshToken(PlatformAccount $account): string
/** /**
* Authenticate with Lemmy API and return user data with JWT * Authenticate with Lemmy API and return user data with JWT
*
* @throws PlatformAuthException * @throws PlatformAuthException
*/ */
/**
* @return array<string, mixed>
*/
public function authenticate(string $instanceUrl, string $username, string $password): array public function authenticate(string $instanceUrl, string $username, string $password): array
{ {
try { try {
$api = new LemmyApiService($instanceUrl); $api = new LemmyApiService($instanceUrl);
$token = $api->login($username, $password); $token = $api->login($username, $password);
if (!$token) { if (! $token) {
// Throw a clean exception that will be caught and handled by the controller // Throw a clean exception that will be caught and handled by the controller
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Invalid credentials'); throw new PlatformAuthException(PlatformEnum::LEMMY, 'Invalid credentials');
} }
@ -75,8 +79,8 @@ public function authenticate(string $instanceUrl, string $username, string $pass
'id' => 0, // Would need API call to get actual user info 'id' => 0, // Would need API call to get actual user info
'display_name' => null, 'display_name' => null,
'bio' => null, 'bio' => null,
] ],
] ],
]; ];
} catch (PlatformAuthException $e) { } catch (PlatformAuthException $e) {
// Re-throw PlatformAuthExceptions as-is to avoid nesting // Re-throw PlatformAuthExceptions as-is to avoid nesting

View file

@ -9,10 +9,12 @@
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\Route; use App\Models\Route;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class DashboardStatsService class DashboardStatsService
{ {
/**
* @return array<string, mixed>
*/
public function getStats(string $period = 'today'): array public function getStats(string $period = 'today'): array
{ {
$dateRange = $this->getDateRange($period); $dateRange = $this->getDateRange($period);
@ -73,6 +75,9 @@ private function getDateRange(string $period): ?array
}; };
} }
/**
* @return array<string, int>
*/
public function getSystemStats(): array public function getSystemStats(): array
{ {
$totalFeeds = Feed::query()->count(); $totalFeeds = Feed::query()->count();

View file

@ -4,9 +4,9 @@
use App\Contracts\ArticleParserInterface; use App\Contracts\ArticleParserInterface;
use App\Models\Feed; use App\Models\Feed;
use App\Services\Parsers\VrtArticleParser;
use App\Services\Parsers\BelgaArticleParser; use App\Services\Parsers\BelgaArticleParser;
use App\Services\Parsers\GuardianArticleParser; use App\Services\Parsers\GuardianArticleParser;
use App\Services\Parsers\VrtArticleParser;
use Exception; use Exception;
class ArticleParserFactory class ArticleParserFactory
@ -26,7 +26,7 @@ class ArticleParserFactory
public static function getParser(string $url): ArticleParserInterface public static function getParser(string $url): ArticleParserInterface
{ {
foreach (self::$parsers as $parserClass) { foreach (self::$parsers as $parserClass) {
$parser = new $parserClass(); $parser = new $parserClass;
if ($parser->canParse($url)) { if ($parser->canParse($url)) {
return $parser; return $parser;
@ -38,21 +38,22 @@ public static function getParser(string $url): ArticleParserInterface
public static function getParserForFeed(Feed $feed, string $parserType = 'article'): ?ArticleParserInterface public static function getParserForFeed(Feed $feed, string $parserType = 'article'): ?ArticleParserInterface
{ {
if (!$feed->provider) { if (! $feed->provider) {
return null; return null;
} }
$providerConfig = config("feed.providers.{$feed->provider}"); $providerConfig = config("feed.providers.{$feed->provider}");
if (!$providerConfig || !isset($providerConfig['parsers'][$parserType])) { if (! $providerConfig || ! isset($providerConfig['parsers'][$parserType])) {
return null; return null;
} }
/** @var class-string<ArticleParserInterface> $parserClass */
$parserClass = $providerConfig['parsers'][$parserType]; $parserClass = $providerConfig['parsers'][$parserType];
if (!class_exists($parserClass)) { if (! class_exists($parserClass)) {
return null; return null;
} }
return new $parserClass(); return new $parserClass;
} }
/** /**
@ -60,18 +61,19 @@ public static function getParserForFeed(Feed $feed, string $parserType = 'articl
*/ */
public static function getSupportedSources(): array public static function getSupportedSources(): array
{ {
return array_map(function($parserClass) { return array_map(function ($parserClass) {
$parser = new $parserClass(); $parser = new $parserClass;
return $parser->getSourceName(); return $parser->getSourceName();
}, self::$parsers); }, self::$parsers);
} }
/** /**
* @param class-string<ArticleParserInterface> $parserClass * @param class-string<ArticleParserInterface> $parserClass
*/ */
public static function registerParser(string $parserClass): void public static function registerParser(string $parserClass): void
{ {
if (!in_array($parserClass, self::$parsers)) { if (! in_array($parserClass, self::$parsers)) {
self::$parsers[] = $parserClass; self::$parsers[] = $parserClass;
} }
} }

View file

@ -9,21 +9,22 @@ class HomepageParserFactory
{ {
public static function getParserForFeed(Feed $feed): ?HomepageParserInterface public static function getParserForFeed(Feed $feed): ?HomepageParserInterface
{ {
if (!$feed->provider) { if (! $feed->provider) {
return null; return null;
} }
$providerConfig = config("feed.providers.{$feed->provider}"); $providerConfig = config("feed.providers.{$feed->provider}");
if (!$providerConfig || !isset($providerConfig['parsers']['homepage'])) { if (! $providerConfig || ! isset($providerConfig['parsers']['homepage'])) {
return null; return null;
} }
/** @var class-string<HomepageParserInterface> $parserClass */
$parserClass = $providerConfig['parsers']['homepage']; $parserClass = $providerConfig['parsers']['homepage'];
if (!class_exists($parserClass)) { if (! class_exists($parserClass)) {
return null; return null;
} }
$language = $feed->language?->short_code ?? 'en'; $language = $feed->language->short_code ?? 'en';
return new $parserClass($language); return new $parserClass($language);
} }

View file

@ -2,8 +2,8 @@
namespace App\Services\Http; namespace App\Services\Http;
use Illuminate\Support\Facades\Http;
use Exception; use Exception;
use Illuminate\Support\Facades\Http;
class HttpFetcher class HttpFetcher
{ {
@ -15,7 +15,7 @@ public static function fetchHtml(string $url): string
try { try {
$response = Http::get($url); $response = Http::get($url);
if (!$response->successful()) { if (! $response->successful()) {
throw new Exception("Failed to fetch URL: {$url} - Status: {$response->status()}"); throw new Exception("Failed to fetch URL: {$url} - Status: {$response->status()}");
} }
@ -23,7 +23,7 @@ public static function fetchHtml(string $url): string
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('HTTP fetch failed', [ logger()->error('HTTP fetch failed', [
'url' => $url, 'url' => $url,
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
throw $e; throw $e;
@ -31,7 +31,7 @@ public static function fetchHtml(string $url): string
} }
/** /**
* @param array<int, string> $urls * @param array<int, string> $urls
* @return array<int, array<string, mixed>> * @return array<int, array<string, mixed>>
*/ */
public static function fetchMultipleUrls(array $urls): array public static function fetchMultipleUrls(array $urls): array
@ -44,8 +44,8 @@ public static function fetchMultipleUrls(array $urls): array
}); });
return collect($responses) return collect($responses)
->filter(fn($response, $index) => isset($urls[$index])) ->filter(fn ($response, $index) => isset($urls[$index]))
->reject(fn($response, $index) => $response instanceof Exception) ->reject(fn ($response, $index) => $response instanceof Exception)
->map(function ($response, $index) use ($urls) { ->map(function ($response, $index) use ($urls) {
$url = $urls[$index]; $url = $urls[$index];
@ -54,14 +54,14 @@ public static function fetchMultipleUrls(array $urls): array
return [ return [
'url' => $url, 'url' => $url,
'html' => $response->body(), 'html' => $response->body(),
'success' => true 'success' => true,
]; ];
} else { } else {
return [ return [
'url' => $url, 'url' => $url,
'html' => null, 'html' => null,
'success' => false, 'success' => false,
'status' => $response->status() 'status' => $response->status(),
]; ];
} }
} catch (Exception) { } catch (Exception) {
@ -69,11 +69,10 @@ public static function fetchMultipleUrls(array $urls): array
'url' => $url, 'url' => $url,
'html' => null, 'html' => null,
'success' => false, 'success' => false,
'error' => 'Exception occurred' 'error' => 'Exception occurred',
]; ];
} }
}) })
->filter(fn($result) => $result !== null)
->toArray(); ->toArray();
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]); logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]);

View file

@ -9,7 +9,7 @@
class LogSaver class LogSaver
{ {
/** /**
* @param array<string, mixed> $context * @param array<string, mixed> $context
*/ */
public function info(string $message, ?PlatformChannel $channel = null, array $context = []): void public function info(string $message, ?PlatformChannel $channel = null, array $context = []): void
{ {
@ -17,7 +17,7 @@ public function info(string $message, ?PlatformChannel $channel = null, array $c
} }
/** /**
* @param array<string, mixed> $context * @param array<string, mixed> $context
*/ */
public function error(string $message, ?PlatformChannel $channel = null, array $context = []): void public function error(string $message, ?PlatformChannel $channel = null, array $context = []): void
{ {
@ -25,7 +25,7 @@ public function error(string $message, ?PlatformChannel $channel = null, array $
} }
/** /**
* @param array<string, mixed> $context * @param array<string, mixed> $context
*/ */
public function warning(string $message, ?PlatformChannel $channel = null, array $context = []): void public function warning(string $message, ?PlatformChannel $channel = null, array $context = []): void
{ {
@ -33,7 +33,7 @@ public function warning(string $message, ?PlatformChannel $channel = null, array
} }
/** /**
* @param array<string, mixed> $context * @param array<string, mixed> $context
*/ */
public function debug(string $message, ?PlatformChannel $channel = null, array $context = []): void public function debug(string $message, ?PlatformChannel $channel = null, array $context = []): void
{ {
@ -41,7 +41,7 @@ public function debug(string $message, ?PlatformChannel $channel = null, array $
} }
/** /**
* @param array<string, mixed> $context * @param array<string, mixed> $context
*/ */
private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
{ {

View file

@ -41,6 +41,6 @@ private function checkOnboardingStatus(): bool
$hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute; $hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute;
return !$hasAllComponents; return ! $hasAllComponents;
} }
} }

View file

@ -10,114 +10,114 @@ public static function extractTitle(string $html): ?string
if (preg_match('/<h1[^>]*class="[^"]*prezly-slate-heading--heading-1[^"]*"[^>]*>([^<]+)<\/h1>/i', $html, $matches)) { if (preg_match('/<h1[^>]*class="[^"]*prezly-slate-heading--heading-1[^"]*"[^>]*>([^<]+)<\/h1>/i', $html, $matches)) {
return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8');
} }
// Try meta title // Try meta title
if (preg_match('/<meta property="og:title" content="([^"]+)"/i', $html, $matches)) { if (preg_match('/<meta property="og:title" content="([^"]+)"/i', $html, $matches)) {
return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8');
} }
// Try any h1 tag // Try any h1 tag
if (preg_match('/<h1[^>]*>([^<]+)<\/h1>/i', $html, $matches)) { if (preg_match('/<h1[^>]*>([^<]+)<\/h1>/i', $html, $matches)) {
return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8');
} }
// Try title tag // Try title tag
if (preg_match('/<title>([^<]+)<\/title>/i', $html, $matches)) { if (preg_match('/<title>([^<]+)<\/title>/i', $html, $matches)) {
return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8');
} }
return null; return null;
} }
public static function extractDescription(string $html): ?string public static function extractDescription(string $html): ?string
{ {
// Try meta description first // Try meta description first
if (preg_match('/<meta property="og:description" content="([^"]+)"/i', $html, $matches)) { if (preg_match('/<meta property="og:description" content="([^"]+)"/i', $html, $matches)) {
return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8');
} }
// Try Belga-specific paragraph class // Try Belga-specific paragraph class
if (preg_match('/<p[^>]*class="[^"]*styles_paragraph__[^"]*"[^>]*>([^<]+(?:<[^\/](?!p)[^>]*>[^<]*<\/[^>]*>[^<]*)*)<\/p>/i', $html, $matches)) { if (preg_match('/<p[^>]*class="[^"]*styles_paragraph__[^"]*"[^>]*>([^<]+(?:<[^\/](?!p)[^>]*>[^<]*<\/[^>]*>[^<]*)*)<\/p>/i', $html, $matches)) {
return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8');
} }
// Try to find first paragraph in article content // Try to find first paragraph in article content
if (preg_match('/<p[^>]*>([^<]+(?:<[^\/](?!p)[^>]*>[^<]*<\/[^>]*>[^<]*)*)<\/p>/i', $html, $matches)) { if (preg_match('/<p[^>]*>([^<]+(?:<[^\/](?!p)[^>]*>[^<]*<\/[^>]*>[^<]*)*)<\/p>/i', $html, $matches)) {
return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8');
} }
return null; return null;
} }
public static function extractFullArticle(string $html): ?string public static function extractFullArticle(string $html): ?string
{ {
// Remove scripts, styles, and other non-content elements // Remove scripts, styles, and other non-content elements
$cleanHtml = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', '', $html); $cleanHtml = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', '', $html);
$cleanHtml = preg_replace('/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/mi', '', $cleanHtml); $cleanHtml = preg_replace('/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/mi', '', $cleanHtml);
// Look for Belga-specific paragraph class // Look for Belga-specific paragraph class
if (preg_match_all('/<p[^>]*class="[^"]*styles_paragraph__[^"]*"[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches)) { if (preg_match_all('/<p[^>]*class="[^"]*styles_paragraph__[^"]*"[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches)) {
$paragraphs = array_map(function($paragraph) { $paragraphs = array_map(function ($paragraph) {
return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8');
}, $matches[1]); }, $matches[1]);
// Filter out empty paragraphs and join with double newlines // Filter out empty paragraphs and join with double newlines
$fullText = implode("\n\n", array_filter($paragraphs, function($p) { $fullText = implode("\n\n", array_filter($paragraphs, function ($p) {
return trim($p) !== ''; return trim($p) !== '';
})); }));
return $fullText ?: null; return $fullText ?: null;
} }
// Fallback: Try to extract from prezly-slate-document section // Fallback: Try to extract from prezly-slate-document section
if (preg_match('/<section[^>]*class="[^"]*prezly-slate-document[^"]*"[^>]*>(.*?)<\/section>/is', $cleanHtml, $sectionMatches)) { if (preg_match('/<section[^>]*class="[^"]*prezly-slate-document[^"]*"[^>]*>(.*?)<\/section>/is', $cleanHtml, $sectionMatches)) {
$sectionHtml = $sectionMatches[1]; $sectionHtml = $sectionMatches[1];
preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $sectionHtml, $matches); preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $sectionHtml, $matches);
if (!empty($matches[1])) { if (! empty($matches[1])) {
$paragraphs = array_map(function($paragraph) { $paragraphs = array_map(function ($paragraph) {
return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8');
}, $matches[1]); }, $matches[1]);
// Filter out empty paragraphs and join with double newlines // Filter out empty paragraphs and join with double newlines
$fullText = implode("\n\n", array_filter($paragraphs, function($p) { $fullText = implode("\n\n", array_filter($paragraphs, function ($p) {
return trim($p) !== ''; return trim($p) !== '';
})); }));
return $fullText ?: null; return $fullText ?: null;
} }
} }
// Final fallback: Extract all paragraph content // Final fallback: Extract all paragraph content
preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches); preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches);
if (!empty($matches[1])) { if (! empty($matches[1])) {
$paragraphs = array_map(function($paragraph) { $paragraphs = array_map(function ($paragraph) {
return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8');
}, $matches[1]); }, $matches[1]);
// Filter out empty paragraphs and join with double newlines // Filter out empty paragraphs and join with double newlines
$fullText = implode("\n\n", array_filter($paragraphs, function($p) { $fullText = implode("\n\n", array_filter($paragraphs, function ($p) {
return trim($p) !== ''; return trim($p) !== '';
})); }));
return $fullText ?: null; return $fullText ?: null;
} }
return null; return null;
} }
public static function extractThumbnail(string $html): ?string public static function extractThumbnail(string $html): ?string
{ {
// Try OpenGraph image first // Try OpenGraph image first
if (preg_match('/<meta property="og:image" content="([^"]+)"/i', $html, $matches)) { if (preg_match('/<meta property="og:image" content="([^"]+)"/i', $html, $matches)) {
return $matches[1]; return $matches[1];
} }
// Try first image in article content // Try first image in article content
if (preg_match('/<img[^>]+src="([^"]+)"/i', $html, $matches)) { if (preg_match('/<img[^>]+src="([^"]+)"/i', $html, $matches)) {
return $matches[1]; return $matches[1];
} }
return null; return null;
} }
@ -133,4 +133,4 @@ public static function extractData(string $html): array
'thumbnail' => self::extractThumbnail($html), 'thumbnail' => self::extractThumbnail($html),
]; ];
} }
} }

View file

@ -20,4 +20,4 @@ public function getSourceName(): string
{ {
return 'Belga News Agency'; return 'Belga News Agency';
} }
} }

View file

@ -30,19 +30,20 @@ public static function extractArticleUrls(string $html): array
->filter(function ($path) use ($blacklistPaths) { ->filter(function ($path) use ($blacklistPaths) {
// Exclude exact matches and paths starting with blacklisted paths // Exclude exact matches and paths starting with blacklisted paths
foreach ($blacklistPaths as $blacklistedPath) { foreach ($blacklistPaths as $blacklistedPath) {
if ($path === $blacklistedPath || str_starts_with($path, $blacklistedPath . '/')) { if ($path === $blacklistedPath || str_starts_with($path, $blacklistedPath.'/')) {
return false; return false;
} }
} }
return true; return true;
}) })
->map(function ($path) { ->map(function ($path) {
// Convert relative paths to absolute URLs // Convert relative paths to absolute URLs
return 'https://www.belganewsagency.eu' . $path; return 'https://www.belganewsagency.eu'.$path;
}) })
->values() ->values()
->toArray(); ->toArray();
return $urls; return $urls;
} }
} }

View file

@ -7,9 +7,14 @@
class BelgaHomepageParserAdapter implements HomepageParserInterface class BelgaHomepageParserAdapter implements HomepageParserInterface
{ {
public function __construct( public function __construct(
private string $language = 'en', private readonly string $language = 'en',
) {} ) {}
public function getLanguage(): string
{
return $this->language;
}
public function canParse(string $url): bool public function canParse(string $url): bool
{ {
return str_contains($url, 'belganewsagency.eu'); return str_contains($url, 'belganewsagency.eu');
@ -29,4 +34,4 @@ public function getSourceName(): string
{ {
return 'Belga News Agency'; return 'Belga News Agency';
} }
} }

View file

@ -50,14 +50,14 @@ public static function extractFullArticle(string $html): ?string
$sectionHtml = $sectionMatches[1]; $sectionHtml = $sectionMatches[1];
preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $sectionHtml, $matches); preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $sectionHtml, $matches);
if (!empty($matches[1])) { if (! empty($matches[1])) {
return self::joinParagraphs($matches[1]); return self::joinParagraphs($matches[1]);
} }
} }
// Fallback: extract all paragraph content // Fallback: extract all paragraph content
preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches); preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches);
if (!empty($matches[1])) { if (! empty($matches[1])) {
return self::joinParagraphs($matches[1]); return self::joinParagraphs($matches[1]);
} }
@ -93,7 +93,7 @@ public static function extractData(string $html): array
} }
/** /**
* @param array<int, string> $paragraphs * @param array<int, string> $paragraphs
*/ */
private static function joinParagraphs(array $paragraphs): ?string private static function joinParagraphs(array $paragraphs): ?string
{ {
@ -107,4 +107,4 @@ private static function joinParagraphs(array $paragraphs): ?string
return $fullText ?: null; return $fullText ?: null;
} }
} }

View file

@ -20,4 +20,4 @@ public function getSourceName(): string
{ {
return 'The Guardian'; return 'The Guardian';
} }
} }

View file

@ -48,13 +48,13 @@ public static function extractFullArticle(string $html): ?string
// Extract all paragraph content // Extract all paragraph content
preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches); preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches);
if (!empty($matches[1])) { if (! empty($matches[1])) {
$paragraphs = array_map(function($paragraph) { $paragraphs = array_map(function ($paragraph) {
return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8');
}, $matches[1]); }, $matches[1]);
// Filter out empty paragraphs and join with double newlines // Filter out empty paragraphs and join with double newlines
$fullText = implode("\n\n", array_filter($paragraphs, function($p) { $fullText = implode("\n\n", array_filter($paragraphs, function ($p) {
return trim($p) !== ''; return trim($p) !== '';
})); }));

View file

@ -20,4 +20,4 @@ public function getSourceName(): string
{ {
return 'VRT News'; return 'VRT News';
} }
} }

View file

@ -10,13 +10,13 @@ class VrtHomepageParser
public static function extractArticleUrls(string $html, string $language = 'en'): array public static function extractArticleUrls(string $html, string $language = 'en'): array
{ {
$escapedLanguage = preg_quote($language, '/'); $escapedLanguage = preg_quote($language, '/');
preg_match_all('/href="(?:https:\/\/www\.vrt\.be)?(\/vrtnws\/' . $escapedLanguage . '\/\d{4}\/\d{2}\/\d{2}\/[^"]+)"/', $html, $matches); preg_match_all('/href="(?:https:\/\/www\.vrt\.be)?(\/vrtnws\/'.$escapedLanguage.'\/\d{4}\/\d{2}\/\d{2}\/[^"]+)"/', $html, $matches);
$urls = collect($matches[1]) $urls = collect($matches[1])
->unique() ->unique()
->map(fn ($path) => 'https://www.vrt.be' . $path) ->map(fn ($path) => 'https://www.vrt.be'.$path)
->toArray(); ->toArray();
return $urls; return $urls;
} }
} }

View file

@ -29,4 +29,4 @@ public function getSourceName(): string
{ {
return 'VRT News'; return 'VRT News';
} }
} }

View file

@ -12,15 +12,13 @@
use App\Modules\Lemmy\Services\LemmyPublisher; use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Log\LogSaver; use App\Services\Log\LogSaver;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use RuntimeException; use RuntimeException;
class ArticlePublishingService class ArticlePublishingService
{ {
public function __construct(private LogSaver $logSaver) public function __construct(private LogSaver $logSaver) {}
{
}
/** /**
* Factory seam to create publisher instances (helps testing without network calls) * Factory seam to create publisher instances (helps testing without network calls)
*/ */
@ -28,9 +26,11 @@ protected function makePublisher(mixed $account): LemmyPublisher
{ {
return new LemmyPublisher($account); return new LemmyPublisher($account);
} }
/** /**
* @param array<string, mixed> $extractedData * @param array<string, mixed> $extractedData
* @return Collection<int, ArticlePublication> * @return Collection<int, ArticlePublication>
*
* @throws PublishException * @throws PublishException
*/ */
public function publishToRoutedChannels(Article $article, array $extractedData): Collection public function publishToRoutedChannels(Article $article, array $extractedData): Collection
@ -60,7 +60,7 @@ public function publishToRoutedChannels(Article $article, array $extractedData):
if (! $account) { if (! $account) {
$this->logSaver->warning('No active account for channel', $channel, [ $this->logSaver->warning('No active account for channel', $channel, [
'article_id' => $article->id, 'article_id' => $article->id,
'route_priority' => $route->priority 'route_priority' => $route->priority,
]); ]);
return null; return null;
@ -68,12 +68,13 @@ public function publishToRoutedChannels(Article $article, array $extractedData):
return $this->publishToChannel($article, $extractedData, $channel, $account); return $this->publishToChannel($article, $extractedData, $channel, $account);
}) })
->filter(); ->filter();
} }
/** /**
* Check if a route matches an article based on keywords * Check if a route matches an article based on keywords
* @param array<string, mixed> $extractedData *
* @param array<string, mixed> $extractedData
*/ */
private function routeMatchesArticle(Route $route, array $extractedData): bool private function routeMatchesArticle(Route $route, array $extractedData): bool
{ {
@ -91,10 +92,10 @@ private function routeMatchesArticle(Route $route, array $extractedData): bool
$articleContent = $extractedData['full_article']; $articleContent = $extractedData['full_article'];
} }
if (isset($extractedData['title'])) { if (isset($extractedData['title'])) {
$articleContent .= ' ' . $extractedData['title']; $articleContent .= ' '.$extractedData['title'];
} }
if (isset($extractedData['description'])) { if (isset($extractedData['description'])) {
$articleContent .= ' ' . $extractedData['description']; $articleContent .= ' '.$extractedData['description'];
} }
// Check if any of the route's keywords match the article content // Check if any of the route's keywords match the article content
@ -109,7 +110,7 @@ private function routeMatchesArticle(Route $route, array $extractedData): bool
} }
/** /**
* @param array<string, mixed> $extractedData * @param array<string, mixed> $extractedData
*/ */
private function publishToChannel(Article $article, array $extractedData, PlatformChannel $channel, mixed $account): ?ArticlePublication private function publishToChannel(Article $article, array $extractedData, PlatformChannel $channel, mixed $account): ?ArticlePublication
{ {
@ -145,14 +146,14 @@ private function publishToChannel(Article $article, array $extractedData, Platfo
]); ]);
$this->logSaver->info('Published to channel via keyword-filtered routing', $channel, [ $this->logSaver->info('Published to channel via keyword-filtered routing', $channel, [
'article_id' => $article->id 'article_id' => $article->id,
]); ]);
return $publication; return $publication;
} catch (Exception $e) { } catch (Exception $e) {
$this->logSaver->warning('Failed to publish to channel', $channel, [ $this->logSaver->warning('Failed to publish to channel', $channel, [
'article_id' => $article->id, 'article_id' => $article->id,
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
return null; return null;

View file

@ -10,7 +10,8 @@
class RoutingValidationService class RoutingValidationService
{ {
/** /**
* @param Collection<int, PlatformChannel> $channels * @param Collection<int, PlatformChannel> $channels
*
* @throws RoutingMismatchException * @throws RoutingMismatchException
*/ */
public function validateLanguageCompatibility(Feed $feed, Collection $channels): void public function validateLanguageCompatibility(Feed $feed, Collection $channels): void
@ -29,4 +30,4 @@ public function validateLanguageCompatibility(Feed $feed, Collection $channels):
} }
} }
} }
} }

View file

@ -3,8 +3,8 @@
namespace App\Services; namespace App\Services;
use App\Models\Feed; use App\Models\Feed;
use App\Models\Route;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\Route;
use App\Models\Setting; use App\Models\Setting;
class SystemStatusService class SystemStatusService
@ -17,22 +17,22 @@ public function getSystemStatus(): array
$reasons = []; $reasons = [];
$isEnabled = true; $isEnabled = true;
if (!Setting::isArticleProcessingEnabled()) { if (! Setting::isArticleProcessingEnabled()) {
$isEnabled = false; $isEnabled = false;
$reasons[] = 'Manually disabled by user'; $reasons[] = 'Manually disabled by user';
} }
if (!Feed::where('is_active', true)->exists()) { if (! Feed::where('is_active', true)->exists()) {
$isEnabled = false; $isEnabled = false;
$reasons[] = 'No active feeds configured'; $reasons[] = 'No active feeds configured';
} }
if (!PlatformChannel::where('is_active', true)->exists()) { if (! PlatformChannel::where('is_active', true)->exists()) {
$isEnabled = false; $isEnabled = false;
$reasons[] = 'No active platform channels configured'; $reasons[] = 'No active platform channels configured';
} }
if (!Route::where('is_active', true)->exists()) { if (! Route::where('is_active', true)->exists()) {
$isEnabled = false; $isEnabled = false;
$reasons[] = 'No active feed-to-channel routes configured'; $reasons[] = 'No active feed-to-channel routes configured';
} }
@ -49,4 +49,4 @@ public function canProcessArticles(): bool
{ {
return $this->getSystemStatus()['is_enabled']; return $this->getSystemStatus()['is_enabled'];
} }
} }

View file

@ -29,6 +29,7 @@
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6", "nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpstan/phpstan-mockery": "^2.0",
"phpunit/phpunit": "^11.5.3" "phpunit/phpunit": "^11.5.3"
}, },
"autoload": { "autoload": {

View file

@ -1,5 +1,6 @@
includes: includes:
- vendor/larastan/larastan/extension.neon - vendor/larastan/larastan/extension.neon
- vendor/phpstan/phpstan-mockery/extension.neon
parameters: parameters:
level: 7 level: 7
@ -10,3 +11,7 @@ parameters:
excludePaths: excludePaths:
- bootstrap/*.php - bootstrap/*.php
- storage/* - storage/*
ignoreErrors:
- identifier: method.alreadyNarrowedType
- identifier: function.alreadyNarrowedType

View file

@ -18,4 +18,4 @@ public function createApplication(): Application
return $app; return $app;
} }
} }

View file

@ -2,11 +2,6 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\Article;
use App\Models\Feed;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\Setting;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -31,7 +26,7 @@ public function test_api_routes_are_publicly_accessible(): void
'/api/v1/feeds', '/api/v1/feeds',
'/api/v1/routing', '/api/v1/routing',
'/api/v1/settings', '/api/v1/settings',
'/api/v1/logs' '/api/v1/logs',
]; ];
foreach ($routes as $route) { foreach ($routes as $route) {
@ -49,7 +44,7 @@ public function test_fallback_route_returns_api_message(): void
$response->assertStatus(404); $response->assertStatus(404);
$response->assertJson([ $response->assertJson([
'message' => 'This is the FFR API backend. Use /api/v1/* endpoints or check the React frontend.', 'message' => 'This is the FFR API backend. Use /api/v1/* endpoints or check the React frontend.',
'api_base' => '/api/v1' 'api_base' => '/api/v1',
]); ]);
} }
} }

View file

@ -27,12 +27,12 @@ public function test_user_model_creates_successfully(): void
{ {
$user = User::factory()->create([ $user = User::factory()->create([
'name' => 'Test User', 'name' => 'Test User',
'email' => 'test@example.com' 'email' => 'test@example.com',
]); ]);
$this->assertDatabaseHas('users', [ $this->assertDatabaseHas('users', [
'name' => 'Test User', 'name' => 'Test User',
'email' => 'test@example.com' 'email' => 'test@example.com',
]); ]);
$this->assertEquals('Test User', $user->name); $this->assertEquals('Test User', $user->name);
@ -43,38 +43,40 @@ public function test_language_model_creates_successfully(): void
{ {
$language = Language::factory()->create([ $language = Language::factory()->create([
'name' => 'English', 'name' => 'English',
'short_code' => 'en' 'short_code' => 'en',
]); ]);
$this->assertDatabaseHas('languages', [ $this->assertDatabaseHas('languages', [
'name' => 'English', 'name' => 'English',
'short_code' => 'en' 'short_code' => 'en',
]); ]);
} }
public function test_platform_instance_model_creates_successfully(): void public function test_platform_instance_model_creates_successfully(): void
{ {
/** @var PlatformInstance $instance */
$instance = PlatformInstance::factory()->create([ $instance = PlatformInstance::factory()->create([
'name' => 'Test Instance', 'name' => 'Test Instance',
'url' => 'https://test.lemmy.world' 'url' => 'https://test.lemmy.world',
]); ]);
$this->assertDatabaseHas('platform_instances', [ $this->assertDatabaseHas('platform_instances', [
'name' => 'Test Instance', 'name' => 'Test Instance',
'url' => 'https://test.lemmy.world' 'url' => 'https://test.lemmy.world',
]); ]);
} }
public function test_platform_account_model_creates_successfully(): void public function test_platform_account_model_creates_successfully(): void
{ {
/** @var PlatformAccount $account */
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'username' => 'testuser', 'username' => 'testuser',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
'username' => 'testuser', 'username' => 'testuser',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals('testuser', $account->username); $this->assertEquals('testuser', $account->username);
@ -84,19 +86,19 @@ public function test_platform_channel_model_creates_successfully(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Channel', 'name' => 'Test Channel',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('platform_channels', [ $this->assertDatabaseHas('platform_channels', [
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Channel', 'name' => 'Test Channel',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($instance->id, $channel->platformInstance->id); $this->assertEquals($instance->id, $channel->platformInstance->id);
@ -106,19 +108,19 @@ public function test_platform_channel_model_creates_successfully(): void
public function test_feed_model_creates_successfully(): void public function test_feed_model_creates_successfully(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed.rss', 'url' => 'https://example.com/feed.rss',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed.rss', 'url' => 'https://example.com/feed.rss',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($language->id, $feed->language->id); $this->assertEquals($language->id, $feed->language->id);
@ -127,19 +129,19 @@ public function test_feed_model_creates_successfully(): void
public function test_article_model_creates_successfully(): void public function test_article_model_creates_successfully(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'title' => 'Test Article', 'title' => 'Test Article',
'url' => 'https://example.com/article', 'url' => 'https://example.com/article',
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
$this->assertDatabaseHas('articles', [ $this->assertDatabaseHas('articles', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'title' => 'Test Article', 'title' => 'Test Article',
'url' => 'https://example.com/article', 'url' => 'https://example.com/article',
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
$this->assertEquals($feed->id, $article->feed->id); $this->assertEquals($feed->id, $article->feed->id);
@ -149,20 +151,20 @@ public function test_article_publication_model_creates_successfully(): void
{ {
$article = Article::factory()->create(); $article = Article::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$publication = ArticlePublication::create([ $publication = ArticlePublication::create([
'article_id' => $article->id, 'article_id' => $article->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'post_id' => 'test-post-123', 'post_id' => 'test-post-123',
'published_at' => now(), 'published_at' => now(),
'published_by' => 'test-user' 'published_by' => 'test-user',
]); ]);
$this->assertDatabaseHas('article_publications', [ $this->assertDatabaseHas('article_publications', [
'article_id' => $article->id, 'article_id' => $article->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'post_id' => 'test-post-123', 'post_id' => 'test-post-123',
'published_by' => 'test-user' 'published_by' => 'test-user',
]); ]);
$this->assertEquals($article->id, $publication->article->id); $this->assertEquals($article->id, $publication->article->id);
@ -173,17 +175,18 @@ public function test_route_model_creates_successfully(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
/** @var Route $route */
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('routes', [ $this->assertDatabaseHas('routes', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($feed->id, $route->feed->id); $this->assertEquals($feed->id, $route->feed->id);
@ -196,16 +199,16 @@ public function test_platform_channel_post_model_creates_successfully(): void
// Likely due to test pollution that's difficult to isolate // Likely due to test pollution that's difficult to isolate
// Commenting out for now since the model works correctly // Commenting out for now since the model works correctly
$this->assertTrue(true); $this->assertTrue(true);
// $post = new PlatformChannelPost([ // $post = new PlatformChannelPost([
// 'platform' => PlatformEnum::LEMMY, // 'platform' => PlatformEnum::LEMMY,
// 'channel_id' => 'technology', // 'channel_id' => 'technology',
// 'post_id' => 'external-post-123', // 'post_id' => 'external-post-123',
// 'title' => 'Test Post', // 'title' => 'Test Post',
// 'url' => 'https://example.com/post', // 'url' => 'https://example.com/post',
// 'posted_at' => now() // 'posted_at' => now()
// ]); // ]);
// $post->save(); // $post->save();
// $this->assertDatabaseHas('platform_channel_posts', [ // $this->assertDatabaseHas('platform_channel_posts', [
@ -222,20 +225,20 @@ public function test_keyword_model_creates_successfully(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$keyword = Keyword::factory() $keyword = Keyword::factory()
->forFeed($feed) ->forFeed($feed)
->forChannel($channel) ->forChannel($channel)
->create([ ->create([
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true, 'is_active' => true,
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]); ]);
} }
@ -245,12 +248,12 @@ public function test_log_model_creates_successfully(): void
'level' => 'info', 'level' => 'info',
'message' => 'Test log message', 'message' => 'Test log message',
'context' => json_encode(['key' => 'value']), 'context' => json_encode(['key' => 'value']),
'logged_at' => now() 'logged_at' => now(),
]); ]);
$this->assertDatabaseHas('logs', [ $this->assertDatabaseHas('logs', [
'level' => 'info', 'level' => 'info',
'message' => 'Test log message' 'message' => 'Test log message',
]); ]);
} }
@ -258,12 +261,12 @@ public function test_setting_model_creates_successfully(): void
{ {
$setting = Setting::create([ $setting = Setting::create([
'key' => 'test_setting', 'key' => 'test_setting',
'value' => 'test_value' 'value' => 'test_value',
]); ]);
$this->assertDatabaseHas('settings', [ $this->assertDatabaseHas('settings', [
'key' => 'test_setting', 'key' => 'test_setting',
'value' => 'test_value' 'value' => 'test_value',
]); ]);
} }
@ -273,7 +276,7 @@ public function test_feed_articles_relationship(): void
$articles = Article::factory()->count(3)->create(['feed_id' => $feed->id]); $articles = Article::factory()->count(3)->create(['feed_id' => $feed->id]);
$this->assertCount(3, $feed->articles); $this->assertCount(3, $feed->articles);
foreach ($articles as $article) { foreach ($articles as $article) {
$this->assertTrue($feed->articles->contains($article)); $this->assertTrue($feed->articles->contains($article));
} }
@ -283,14 +286,14 @@ public function test_platform_account_channels_many_to_many_relationship(): void
{ {
$account = PlatformAccount::factory()->create(); $account = PlatformAccount::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
// Test the pivot table relationship // Test the pivot table relationship
$account->channels()->attach($channel->id, ['is_active' => true, 'priority' => 1]); $account->channels()->attach($channel->id, ['is_active' => true, 'priority' => 1]);
$this->assertDatabaseHas('platform_account_channels', [ $this->assertDatabaseHas('platform_account_channels', [
'platform_account_id' => $account->id, 'platform_account_id' => $account->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -298,14 +301,14 @@ public function test_language_platform_instances_relationship(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$instances = PlatformInstance::factory()->count(2)->create(); $instances = PlatformInstance::factory()->count(2)->create();
// Attach language to instances via pivot table // Attach language to instances via pivot table
foreach ($instances as $instance) { foreach ($instances as $instance) {
$language->platformInstances()->attach($instance->id, ['platform_language_id' => rand(1, 100)]); $language->platformInstances()->attach($instance->id, ['platform_language_id' => rand(1, 100)]);
} }
$this->assertCount(2, $language->platformInstances); $this->assertCount(2, $language->platformInstances);
foreach ($instances as $instance) { foreach ($instances as $instance) {
$this->assertTrue($language->platformInstances->contains($instance)); $this->assertTrue($language->platformInstances->contains($instance));
} }
@ -316,15 +319,15 @@ public function test_model_soft_deletes_work_correctly(): void
// Test models that might use soft deletes // Test models that might use soft deletes
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$feedId = $feed->id; $feedId = $feed->id;
$feed->delete(); $feed->delete();
// Should not find with normal query if soft deleted // Should not find with normal query if soft deleted
$this->assertNull(Feed::find($feedId)); $this->assertNull(Feed::find($feedId));
// Should find with withTrashed if model uses soft deletes // Should find with withTrashed if model uses soft deletes
if (method_exists($feed, 'withTrashed')) { if (method_exists($feed, 'withTrashed')) {
$this->assertNotNull(Feed::withTrashed()->find($feedId)); $this->assertNotNull(Feed::withTrashed()->find($feedId)); // @phpstan-ignore staticMethod.notFound
} }
} }
@ -332,7 +335,7 @@ public function test_database_constraints_are_enforced(): void
{ {
// Test foreign key constraints // Test foreign key constraints
$this->expectException(\Illuminate\Database\QueryException::class); $this->expectException(\Illuminate\Database\QueryException::class);
// Try to create article with non-existent feed_id // Try to create article with non-existent feed_id
Article::factory()->create(['feed_id' => 99999]); Article::factory()->create(['feed_id' => 99999]);
} }
@ -357,4 +360,4 @@ public function test_all_factories_work_correctly(): void
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Model::class, $model); $this->assertInstanceOf(\Illuminate\Database\Eloquent\Model::class, $model);
} }
} }
} }

View file

@ -78,7 +78,7 @@ public function test_command_skips_when_article_processing_disabled(): void
Queue::fake(); Queue::fake();
Setting::create([ Setting::create([
'key' => 'article_processing_enabled', 'key' => 'article_processing_enabled',
'value' => '0' 'value' => '0',
]); ]);
// Act // Act

View file

@ -2,9 +2,7 @@
namespace Tests\Feature\Http\Console\Commands; namespace Tests\Feature\Http\Console\Commands;
use App\Jobs\SyncChannelPostsJob;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Illuminate\Testing\PendingCommand; use Illuminate\Testing\PendingCommand;
use Tests\TestCase; use Tests\TestCase;
@ -36,8 +34,9 @@ public function test_command_returns_failure_exit_code_for_unsupported_platform(
public function test_command_accepts_lemmy_platform_argument(): void public function test_command_accepts_lemmy_platform_argument(): void
{ {
// Act - Test that the command accepts lemmy as a valid platform argument // Act - Test that the command accepts lemmy as a valid platform argument
/** @var PendingCommand $exitCode */
$exitCode = $this->artisan('channel:sync lemmy'); $exitCode = $this->artisan('channel:sync lemmy');
// Assert - Command should succeed (not fail with argument validation error) // Assert - Command should succeed (not fail with argument validation error)
$exitCode->assertSuccessful(); $exitCode->assertSuccessful();
$exitCode->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels'); $exitCode->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels');
@ -46,10 +45,11 @@ public function test_command_accepts_lemmy_platform_argument(): void
public function test_command_handles_default_platform(): void public function test_command_handles_default_platform(): void
{ {
// Act - Test that the command works with default platform (should be lemmy) // Act - Test that the command works with default platform (should be lemmy)
/** @var PendingCommand $exitCode */
$exitCode = $this->artisan('channel:sync'); $exitCode = $this->artisan('channel:sync');
// Assert - Command should succeed with default platform // Assert - Command should succeed with default platform
$exitCode->assertSuccessful(); $exitCode->assertSuccessful();
$exitCode->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels'); $exitCode->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels');
} }
} }

View file

@ -4,7 +4,6 @@
use App\Models\Article; use App\Models\Article;
use App\Models\Feed; use App\Models\Feed;
use App\Models\Setting;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -33,7 +32,7 @@ public function test_index_returns_successful_response(): void
'publishing_approvals_enabled', 'publishing_approvals_enabled',
], ],
], ],
'message' 'message',
]); ]);
} }
@ -53,7 +52,7 @@ public function test_index_returns_articles_with_pagination(): void
'total' => 25, 'total' => 25,
'last_page' => 3, 'last_page' => 3,
], ],
] ],
]); ]);
$this->assertCount(10, $response->json('data.articles')); $this->assertCount(10, $response->json('data.articles'));
@ -74,30 +73,30 @@ public function test_index_respects_per_page_limit(): void
'pagination' => [ 'pagination' => [
'per_page' => 100, // Should be capped at 100 'per_page' => 100, // Should be capped at 100
], ],
] ],
]); ]);
} }
public function test_index_orders_articles_by_created_at_desc(): void public function test_index_orders_articles_by_created_at_desc(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$firstArticle = Article::factory()->create([ $firstArticle = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'created_at' => now()->subHours(2), 'created_at' => now()->subHours(2),
'title' => 'First Article' 'title' => 'First Article',
]); ]);
$secondArticle = Article::factory()->create([ $secondArticle = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'created_at' => now()->subHour(), 'created_at' => now()->subHour(),
'title' => 'Second Article' 'title' => 'Second Article',
]); ]);
$response = $this->getJson('/api/v1/articles'); $response = $this->getJson('/api/v1/articles');
$response->assertStatus(200); $response->assertStatus(200);
$articles = $response->json('data.articles'); $articles = $response->json('data.articles');
$this->assertEquals('Second Article', $articles[0]['title']); $this->assertEquals('Second Article', $articles[0]['title']);
$this->assertEquals('First Article', $articles[1]['title']); $this->assertEquals('First Article', $articles[1]['title']);
@ -108,7 +107,7 @@ public function test_approve_article_successfully(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
$response = $this->postJson("/api/v1/articles/{$article->id}/approve"); $response = $this->postJson("/api/v1/articles/{$article->id}/approve");
@ -116,7 +115,7 @@ public function test_approve_article_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Article approved and queued for publishing.' 'message' => 'Article approved and queued for publishing.',
]); ]);
$article->refresh(); $article->refresh();
@ -135,7 +134,7 @@ public function test_reject_article_successfully(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
$response = $this->postJson("/api/v1/articles/{$article->id}/reject"); $response = $this->postJson("/api/v1/articles/{$article->id}/reject");
@ -143,7 +142,7 @@ public function test_reject_article_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Article rejected.' 'message' => 'Article rejected.',
]); ]);
$article->refresh(); $article->refresh();
@ -165,9 +164,9 @@ public function test_index_includes_settings(): void
->assertJsonStructure([ ->assertJsonStructure([
'data' => [ 'data' => [
'settings' => [ 'settings' => [
'publishing_approvals_enabled' 'publishing_approvals_enabled',
] ],
] ],
]); ]);
} }
} }

View file

@ -38,7 +38,7 @@ public function test_stats_returns_successful_response(): void
'available_periods', 'available_periods',
'current_period', 'current_period',
], ],
'message' 'message',
]); ]);
} }
@ -54,7 +54,7 @@ public function test_stats_with_different_periods(): void
'success' => true, 'success' => true,
'data' => [ 'data' => [
'current_period' => $period, 'current_period' => $period,
] ],
]); ]);
} }
} }
@ -80,7 +80,7 @@ public function test_stats_with_sample_data(): void
ArticlePublication::factory()->create([ ArticlePublication::factory()->create([
'article_id' => $articles->first()->id, 'article_id' => $articles->first()->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'published_at' => now() 'published_at' => now(),
]); ]);
$response = $this->getJson('/api/v1/dashboard/stats?period=all'); $response = $this->getJson('/api/v1/dashboard/stats?period=all');
@ -93,7 +93,7 @@ public function test_stats_with_sample_data(): void
'articles_fetched' => $initialArticles + 3, 'articles_fetched' => $initialArticles + 3,
'articles_published' => $initialPublications + 1, 'articles_published' => $initialPublications + 1,
], ],
] ],
]); ]);
// Just verify structure and that we have more items than we started with // Just verify structure and that we have more items than we started with
@ -126,7 +126,7 @@ public function test_stats_returns_empty_data_with_no_records(): void
'total_routes' => 0, 'total_routes' => 0,
'active_routes' => 0, 'active_routes' => 0,
], ],
] ],
]); ]);
} }
} }

View file

@ -20,9 +20,9 @@ public function test_index_returns_successful_response(): void
'success', 'success',
'data' => [ 'data' => [
'feeds', 'feeds',
'pagination' 'pagination',
], ],
'message' 'message',
]); ]);
} }
@ -36,7 +36,7 @@ public function test_index_returns_feeds_ordered_by_active_status_then_name(): v
$response->assertStatus(200); $response->assertStatus(200);
$feeds = $response->json('data.feeds'); $feeds = $response->json('data.feeds');
// First should be active feeds in alphabetical order // First should be active feeds in alphabetical order
$this->assertEquals('A Feed', $feeds[0]['name']); $this->assertEquals('A Feed', $feeds[0]['name']);
$this->assertTrue($feeds[0]['is_active']); $this->assertTrue($feeds[0]['is_active']);
@ -69,7 +69,7 @@ public function test_store_creates_vrt_feed_successfully(): void
'url' => 'https://www.vrt.be/vrtnws/en/', 'url' => 'https://www.vrt.be/vrtnws/en/',
'type' => 'website', 'type' => 'website',
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -101,7 +101,7 @@ public function test_store_creates_belga_feed_successfully(): void
'url' => 'https://www.belganewsagency.eu/', 'url' => 'https://www.belganewsagency.eu/',
'type' => 'website', 'type' => 'website',
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -133,7 +133,7 @@ public function test_store_creates_guardian_feed_successfully(): void
'url' => 'https://www.theguardian.com/international/rss', 'url' => 'https://www.theguardian.com/international/rss',
'type' => 'rss', 'type' => 'rss',
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -160,7 +160,7 @@ public function test_store_sets_default_active_status(): void
->assertJson([ ->assertJson([
'data' => [ 'data' => [
'is_active' => true, // Should default to true 'is_active' => true, // Should default to true
] ],
]); ]);
} }
@ -175,7 +175,7 @@ public function test_store_validates_required_fields(): void
public function test_store_rejects_invalid_provider(): void public function test_store_rejects_invalid_provider(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$feedData = [ $feedData = [
'name' => 'Invalid Feed', 'name' => 'Invalid Feed',
'provider' => 'invalid', 'provider' => 'invalid',
@ -201,7 +201,7 @@ public function test_show_returns_feed_successfully(): void
'data' => [ 'data' => [
'id' => $feed->id, 'id' => $feed->id,
'name' => $feed->name, 'name' => $feed->name,
] ],
]); ]);
} }
@ -216,7 +216,7 @@ public function test_update_modifies_feed_successfully(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$feed = Feed::factory()->language($language)->create(['name' => 'Original Name']); $feed = Feed::factory()->language($language)->create(['name' => 'Original Name']);
$updateData = [ $updateData = [
'name' => 'Updated Name', 'name' => 'Updated Name',
'url' => $feed->url, 'url' => $feed->url,
@ -232,7 +232,7 @@ public function test_update_modifies_feed_successfully(): void
'message' => 'Feed updated successfully!', 'message' => 'Feed updated successfully!',
'data' => [ 'data' => [
'name' => 'Updated Name', 'name' => 'Updated Name',
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -245,7 +245,7 @@ public function test_update_preserves_active_status_when_not_provided(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$feed = Feed::factory()->language($language)->create(['is_active' => false]); $feed = Feed::factory()->language($language)->create(['is_active' => false]);
$updateData = [ $updateData = [
'name' => $feed->name, 'name' => $feed->name,
'url' => $feed->url, 'url' => $feed->url,
@ -260,7 +260,7 @@ public function test_update_preserves_active_status_when_not_provided(): void
->assertJson([ ->assertJson([
'data' => [ 'data' => [
'is_active' => false, // Should preserve original value 'is_active' => false, // Should preserve original value
] ],
]); ]);
} }
@ -298,7 +298,7 @@ public function test_toggle_activates_inactive_feed(): void
'message' => 'Feed activated successfully!', 'message' => 'Feed activated successfully!',
'data' => [ 'data' => [
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -319,7 +319,7 @@ public function test_toggle_deactivates_active_feed(): void
'message' => 'Feed deactivated successfully!', 'message' => 'Feed deactivated successfully!',
'data' => [ 'data' => [
'is_active' => false, 'is_active' => false,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -334,4 +334,4 @@ public function test_toggle_returns_404_for_nonexistent_feed(): void
$response->assertStatus(404); $response->assertStatus(404);
} }
} }

View file

@ -14,21 +14,23 @@ class KeywordsControllerTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
protected Feed $feed; protected Feed $feed;
protected PlatformChannel $channel; protected PlatformChannel $channel;
protected Route $route; protected Route $route;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->feed = Feed::factory()->create(); $this->feed = Feed::factory()->create();
$this->channel = PlatformChannel::factory()->create(); $this->channel = PlatformChannel::factory()->create();
$this->route = Route::create([ $this->route = Route::create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id, 'platform_channel_id' => $this->channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
} }
@ -38,7 +40,7 @@ public function test_can_get_keywords_for_route(): void
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id, 'platform_channel_id' => $this->channel->id,
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true 'is_active' => true,
]); ]);
$response = $this->getJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords"); $response = $this->getJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords");
@ -52,9 +54,9 @@ public function test_can_get_keywords_for_route(): void
'keyword', 'keyword',
'is_active', 'is_active',
'feed_id', 'feed_id',
'platform_channel_id' 'platform_channel_id',
] ],
] ],
]) ])
->assertJsonPath('data.0.keyword', 'test keyword'); ->assertJsonPath('data.0.keyword', 'test keyword');
} }
@ -63,7 +65,7 @@ public function test_can_create_keyword_for_route(): void
{ {
$keywordData = [ $keywordData = [
'keyword' => 'new keyword', 'keyword' => 'new keyword',
'is_active' => true 'is_active' => true,
]; ];
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", $keywordData); $response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", $keywordData);
@ -76,8 +78,8 @@ public function test_can_create_keyword_for_route(): void
'keyword', 'keyword',
'is_active', 'is_active',
'feed_id', 'feed_id',
'platform_channel_id' 'platform_channel_id',
] ],
]) ])
->assertJsonPath('data.keyword', 'new keyword') ->assertJsonPath('data.keyword', 'new keyword')
->assertJsonPath('data.is_active', true); ->assertJsonPath('data.is_active', true);
@ -86,7 +88,7 @@ public function test_can_create_keyword_for_route(): void
'keyword' => 'new keyword', 'keyword' => 'new keyword',
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id, 'platform_channel_id' => $this->channel->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -95,11 +97,11 @@ public function test_cannot_create_duplicate_keyword_for_route(): void
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id, 'platform_channel_id' => $this->channel->id,
'keyword' => 'duplicate keyword' 'keyword' => 'duplicate keyword',
]); ]);
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", [ $response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", [
'keyword' => 'duplicate keyword' 'keyword' => 'duplicate keyword',
]); ]);
$response->assertStatus(409) $response->assertStatus(409)
@ -109,14 +111,15 @@ public function test_cannot_create_duplicate_keyword_for_route(): void
public function test_can_update_keyword(): void public function test_can_update_keyword(): void
{ {
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([ $keyword = Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id, 'platform_channel_id' => $this->channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$response = $this->putJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}", [ $response = $this->putJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}", [
'is_active' => false 'is_active' => false,
]); ]);
$response->assertStatus(200) $response->assertStatus(200)
@ -124,15 +127,16 @@ public function test_can_update_keyword(): void
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'id' => $keyword->id, 'id' => $keyword->id,
'is_active' => false 'is_active' => false,
]); ]);
} }
public function test_can_delete_keyword(): void public function test_can_delete_keyword(): void
{ {
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([ $keyword = Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id 'platform_channel_id' => $this->channel->id,
]); ]);
$response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}"); $response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}");
@ -140,16 +144,17 @@ public function test_can_delete_keyword(): void
$response->assertStatus(200); $response->assertStatus(200);
$this->assertDatabaseMissing('keywords', [ $this->assertDatabaseMissing('keywords', [
'id' => $keyword->id 'id' => $keyword->id,
]); ]);
} }
public function test_can_toggle_keyword(): void public function test_can_toggle_keyword(): void
{ {
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([ $keyword = Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id, 'platform_channel_id' => $this->channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}/toggle"); $response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}/toggle");
@ -159,7 +164,7 @@ public function test_can_toggle_keyword(): void
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'id' => $keyword->id, 'id' => $keyword->id,
'is_active' => false 'is_active' => false,
]); ]);
} }
@ -167,10 +172,11 @@ public function test_cannot_access_keyword_from_different_route(): void
{ {
$otherFeed = Feed::factory()->create(); $otherFeed = Feed::factory()->create();
$otherChannel = PlatformChannel::factory()->create(); $otherChannel = PlatformChannel::factory()->create();
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([ $keyword = Keyword::factory()->create([
'feed_id' => $otherFeed->id, 'feed_id' => $otherFeed->id,
'platform_channel_id' => $otherChannel->id 'platform_channel_id' => $otherChannel->id,
]); ]);
$response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}"); $response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}");
@ -186,4 +192,4 @@ public function test_validates_required_fields(): void
$response->assertStatus(422) $response->assertStatus(422)
->assertJsonValidationErrors(['keyword']); ->assertJsonValidationErrors(['keyword']);
} }
} }

View file

@ -37,7 +37,7 @@ public function test_index_returns_successful_response(): void
'context', 'context',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
], ],
'pagination' => [ 'pagination' => [
'current_page', 'current_page',
@ -46,12 +46,12 @@ public function test_index_returns_successful_response(): void
'total', 'total',
'from', 'from',
'to', 'to',
] ],
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Logs retrieved successfully.' 'message' => 'Logs retrieved successfully.',
]); ]);
} }
@ -64,9 +64,9 @@ public function test_index_orders_logs_by_created_at_desc(): void
$response = $this->getJson('/api/v1/logs'); $response = $this->getJson('/api/v1/logs');
$response->assertStatus(200); $response->assertStatus(200);
$logs = $response->json('data.logs'); $logs = $response->json('data.logs');
$this->assertEquals($newestLog->id, $logs[0]['id']); $this->assertEquals($newestLog->id, $logs[0]['id']);
$this->assertEquals($newLog->id, $logs[1]['id']); $this->assertEquals($newLog->id, $logs[1]['id']);
$this->assertEquals($oldLog->id, $logs[2]['id']); $this->assertEquals($oldLog->id, $logs[2]['id']);
@ -81,9 +81,9 @@ public function test_index_filters_by_level(): void
$response = $this->getJson('/api/v1/logs?level=error'); $response = $this->getJson('/api/v1/logs?level=error');
$response->assertStatus(200); $response->assertStatus(200);
$logs = $response->json('data.logs'); $logs = $response->json('data.logs');
$this->assertCount(1, $logs); $this->assertCount(1, $logs);
$this->assertEquals('error', $logs[0]['level']); $this->assertEquals('error', $logs[0]['level']);
} }
@ -95,10 +95,10 @@ public function test_index_respects_per_page_parameter(): void
$response = $this->getJson('/api/v1/logs?per_page=5'); $response = $this->getJson('/api/v1/logs?per_page=5');
$response->assertStatus(200); $response->assertStatus(200);
$logs = $response->json('data.logs'); $logs = $response->json('data.logs');
$pagination = $response->json('data.pagination'); $pagination = $response->json('data.pagination');
$this->assertCount(5, $logs); $this->assertCount(5, $logs);
$this->assertEquals(5, $pagination['per_page']); $this->assertEquals(5, $pagination['per_page']);
$this->assertEquals(15, $pagination['total']); $this->assertEquals(15, $pagination['total']);
@ -112,9 +112,9 @@ public function test_index_limits_per_page_to_maximum(): void
$response = $this->getJson('/api/v1/logs?per_page=150'); $response = $this->getJson('/api/v1/logs?per_page=150');
$response->assertStatus(200); $response->assertStatus(200);
$pagination = $response->json('data.pagination'); $pagination = $response->json('data.pagination');
// Should be limited to 100 as per controller logic // Should be limited to 100 as per controller logic
$this->assertEquals(100, $pagination['per_page']); $this->assertEquals(100, $pagination['per_page']);
} }
@ -126,9 +126,9 @@ public function test_index_uses_default_per_page_when_not_specified(): void
$response = $this->getJson('/api/v1/logs'); $response = $this->getJson('/api/v1/logs');
$response->assertStatus(200); $response->assertStatus(200);
$pagination = $response->json('data.pagination'); $pagination = $response->json('data.pagination');
// Should use default of 20 // Should use default of 20
$this->assertEquals(20, $pagination['per_page']); $this->assertEquals(20, $pagination['per_page']);
} }
@ -147,8 +147,8 @@ public function test_index_handles_empty_logs(): void
'total' => 0, 'total' => 0,
'current_page' => 1, 'current_page' => 1,
'last_page' => 1, 'last_page' => 1,
] ],
] ],
]); ]);
} }
@ -159,7 +159,7 @@ public function test_index_pagination_works_correctly(): void
// Test first page // Test first page
$response = $this->getJson('/api/v1/logs?per_page=10&page=1'); $response = $this->getJson('/api/v1/logs?per_page=10&page=1');
$response->assertStatus(200); $response->assertStatus(200);
$pagination = $response->json('data.pagination'); $pagination = $response->json('data.pagination');
$this->assertEquals(1, $pagination['current_page']); $this->assertEquals(1, $pagination['current_page']);
$this->assertEquals(3, $pagination['last_page']); $this->assertEquals(3, $pagination['last_page']);
@ -169,7 +169,7 @@ public function test_index_pagination_works_correctly(): void
// Test second page // Test second page
$response = $this->getJson('/api/v1/logs?per_page=10&page=2'); $response = $this->getJson('/api/v1/logs?per_page=10&page=2');
$response->assertStatus(200); $response->assertStatus(200);
$pagination = $response->json('data.pagination'); $pagination = $response->json('data.pagination');
$this->assertEquals(2, $pagination['current_page']); $this->assertEquals(2, $pagination['current_page']);
$this->assertEquals(11, $pagination['from']); $this->assertEquals(11, $pagination['from']);
@ -186,15 +186,15 @@ public function test_index_with_multiple_log_levels(): void
$response = $this->getJson('/api/v1/logs'); $response = $this->getJson('/api/v1/logs');
$response->assertStatus(200); $response->assertStatus(200);
$logs = $response->json('data.logs'); $logs = $response->json('data.logs');
$this->assertCount(4, $logs); $this->assertCount(4, $logs);
$levels = array_column($logs, 'level'); $levels = array_column($logs, 'level');
$this->assertContains('error', $levels); $this->assertContains('error', $levels);
$this->assertContains('warning', $levels); $this->assertContains('warning', $levels);
$this->assertContains('info', $levels); $this->assertContains('info', $levels);
$this->assertContains('debug', $levels); $this->assertContains('debug', $levels);
} }
} }

View file

@ -19,7 +19,7 @@ class OnboardingControllerTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
// Create a language for testing // Create a language for testing
Language::factory()->create([ Language::factory()->create([
'id' => 1, 'id' => 1,
@ -30,7 +30,7 @@ protected function setUp(): void
]); ]);
} }
public function test_status_shows_needs_onboarding_when_no_components_exist() public function test_status_shows_needs_onboarding_when_no_components_exist(): void
{ {
$response = $this->getJson('/api/v1/onboarding/status'); $response = $this->getJson('/api/v1/onboarding/status');
@ -49,7 +49,7 @@ public function test_status_shows_needs_onboarding_when_no_components_exist()
]); ]);
} }
public function test_status_shows_feed_step_when_platform_account_exists() public function test_status_shows_feed_step_when_platform_account_exists(): void
{ {
PlatformAccount::factory()->create(['is_active' => true]); PlatformAccount::factory()->create(['is_active' => true]);
@ -69,7 +69,7 @@ public function test_status_shows_feed_step_when_platform_account_exists()
]); ]);
} }
public function test_status_shows_channel_step_when_platform_account_and_feed_exist() public function test_status_shows_channel_step_when_platform_account_and_feed_exist(): void
{ {
$language = Language::first(); $language = Language::first();
PlatformAccount::factory()->create(['is_active' => true]); PlatformAccount::factory()->create(['is_active' => true]);
@ -91,7 +91,7 @@ public function test_status_shows_channel_step_when_platform_account_and_feed_ex
]); ]);
} }
public function test_status_shows_route_step_when_platform_account_feed_and_channel_exist() public function test_status_shows_route_step_when_platform_account_feed_and_channel_exist(): void
{ {
$language = Language::first(); $language = Language::first();
PlatformAccount::factory()->create(['is_active' => true]); PlatformAccount::factory()->create(['is_active' => true]);
@ -114,7 +114,7 @@ public function test_status_shows_route_step_when_platform_account_feed_and_chan
]); ]);
} }
public function test_status_shows_no_onboarding_needed_when_all_components_exist() public function test_status_shows_no_onboarding_needed_when_all_components_exist(): void
{ {
$language = Language::first(); $language = Language::first();
PlatformAccount::factory()->create(['is_active' => true]); PlatformAccount::factory()->create(['is_active' => true]);
@ -138,7 +138,7 @@ public function test_status_shows_no_onboarding_needed_when_all_components_exist
]); ]);
} }
public function test_status_shows_no_onboarding_needed_when_skipped() public function test_status_shows_no_onboarding_needed_when_skipped(): void
{ {
// No components exist but onboarding is skipped // No components exist but onboarding is skipped
Setting::create([ Setting::create([
@ -163,7 +163,7 @@ public function test_status_shows_no_onboarding_needed_when_skipped()
]); ]);
} }
public function test_options_returns_languages_and_platform_instances() public function test_options_returns_languages_and_platform_instances(): void
{ {
PlatformInstance::factory()->create([ PlatformInstance::factory()->create([
'platform' => 'lemmy', 'platform' => 'lemmy',
@ -179,34 +179,34 @@ public function test_options_returns_languages_and_platform_instances()
'success', 'success',
'data' => [ 'data' => [
'languages' => [ 'languages' => [
'*' => ['id', 'short_code', 'name', 'native_name', 'is_active'] '*' => ['id', 'short_code', 'name', 'native_name', 'is_active'],
], ],
'platform_instances' => [ 'platform_instances' => [
'*' => ['id', 'platform', 'url', 'name', 'description', 'is_active'] '*' => ['id', 'platform', 'url', 'name', 'description', 'is_active'],
] ],
] ],
]); ]);
} }
public function test_complete_onboarding_returns_success() public function test_complete_onboarding_returns_success(): void
{ {
$response = $this->postJson('/api/v1/onboarding/complete'); $response = $this->postJson('/api/v1/onboarding/complete');
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'data' => ['completed' => true] 'data' => ['completed' => true],
]); ]);
} }
public function test_skip_onboarding_creates_setting() public function test_skip_onboarding_creates_setting(): void
{ {
$response = $this->postJson('/api/v1/onboarding/skip'); $response = $this->postJson('/api/v1/onboarding/skip');
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'data' => ['skipped' => true] 'data' => ['skipped' => true],
]); ]);
$this->assertDatabaseHas('settings', [ $this->assertDatabaseHas('settings', [
@ -215,7 +215,7 @@ public function test_skip_onboarding_creates_setting()
]); ]);
} }
public function test_skip_onboarding_updates_existing_setting() public function test_skip_onboarding_updates_existing_setting(): void
{ {
// Create existing setting with false value // Create existing setting with false value
Setting::create([ Setting::create([
@ -236,7 +236,7 @@ public function test_skip_onboarding_updates_existing_setting()
$this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count()); $this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count());
} }
public function test_reset_skip_removes_setting() public function test_reset_skip_removes_setting(): void
{ {
// Create skipped setting // Create skipped setting
Setting::create([ Setting::create([
@ -249,7 +249,7 @@ public function test_reset_skip_removes_setting()
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'data' => ['reset' => true] 'data' => ['reset' => true],
]); ]);
$this->assertDatabaseMissing('settings', [ $this->assertDatabaseMissing('settings', [
@ -257,18 +257,18 @@ public function test_reset_skip_removes_setting()
]); ]);
} }
public function test_reset_skip_works_when_no_setting_exists() public function test_reset_skip_works_when_no_setting_exists(): void
{ {
$response = $this->postJson('/api/v1/onboarding/reset-skip'); $response = $this->postJson('/api/v1/onboarding/reset-skip');
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'data' => ['reset' => true] 'data' => ['reset' => true],
]); ]);
} }
public function test_onboarding_flow_integration() public function test_onboarding_flow_integration(): void
{ {
// 1. Initial status - needs onboarding // 1. Initial status - needs onboarding
$response = $this->getJson('/api/v1/onboarding/status'); $response = $this->getJson('/api/v1/onboarding/status');
@ -290,4 +290,4 @@ public function test_onboarding_flow_integration()
$response = $this->getJson('/api/v1/onboarding/status'); $response = $this->getJson('/api/v1/onboarding/status');
$response->assertJson(['data' => ['needs_onboarding' => true, 'onboarding_skipped' => false]]); $response->assertJson(['data' => ['needs_onboarding' => true, 'onboarding_skipped' => false]]);
} }
} }

View file

@ -33,12 +33,12 @@ public function test_index_returns_successful_response(): void
'is_active', 'is_active',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform accounts retrieved successfully.' 'message' => 'Platform accounts retrieved successfully.',
]); ]);
} }
@ -75,11 +75,11 @@ public function test_store_creates_platform_account_successfully(): void
'is_active', 'is_active',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform account created successfully!' 'message' => 'Platform account created successfully!',
]); ]);
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
@ -115,7 +115,7 @@ public function test_show_returns_platform_account_successfully(): void
'is_active', 'is_active',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
@ -123,7 +123,7 @@ public function test_show_returns_platform_account_successfully(): void
'data' => [ 'data' => [
'id' => $account->id, 'id' => $account->id,
'username' => $account->username, 'username' => $account->username,
] ],
]); ]);
} }
@ -134,7 +134,7 @@ public function test_update_modifies_platform_account_successfully(): void
$updateData = [ $updateData = [
'instance_url' => 'https://updated.example.com', 'instance_url' => 'https://updated.example.com',
'username' => 'updateduser', 'username' => 'updateduser',
'settings' => ['updated' => 'value'] 'settings' => ['updated' => 'value'],
]; ];
$response = $this->putJson("/api/v1/platform-accounts/{$account->id}", $updateData); $response = $this->putJson("/api/v1/platform-accounts/{$account->id}", $updateData);
@ -142,7 +142,7 @@ public function test_update_modifies_platform_account_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform account updated successfully!' 'message' => 'Platform account updated successfully!',
]); ]);
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
@ -161,11 +161,11 @@ public function test_destroy_deletes_platform_account_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform account deleted successfully!' 'message' => 'Platform account deleted successfully!',
]); ]);
$this->assertDatabaseMissing('platform_accounts', [ $this->assertDatabaseMissing('platform_accounts', [
'id' => $account->id 'id' => $account->id,
]); ]);
} }
@ -182,7 +182,7 @@ public function test_set_active_activates_platform_account(): void
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
'id' => $account->id, 'id' => $account->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -190,12 +190,12 @@ public function test_set_active_deactivates_other_accounts_of_same_platform(): v
{ {
$activeAccount = PlatformAccount::factory()->create([ $activeAccount = PlatformAccount::factory()->create([
'platform' => 'lemmy', 'platform' => 'lemmy',
'is_active' => true 'is_active' => true,
]); ]);
$newAccount = PlatformAccount::factory()->create([ $newAccount = PlatformAccount::factory()->create([
'platform' => 'lemmy', 'platform' => 'lemmy',
'is_active' => false 'is_active' => false,
]); ]);
$response = $this->postJson("/api/v1/platform-accounts/{$newAccount->id}/set-active"); $response = $this->postJson("/api/v1/platform-accounts/{$newAccount->id}/set-active");
@ -204,12 +204,12 @@ public function test_set_active_deactivates_other_accounts_of_same_platform(): v
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
'id' => $activeAccount->id, 'id' => $activeAccount->id,
'is_active' => false 'is_active' => false,
]); ]);
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
'id' => $newAccount->id, 'id' => $newAccount->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
} }

View file

@ -34,24 +34,24 @@ public function test_index_returns_successful_response(): void
'is_active', 'is_active',
'created_at', 'created_at',
'updated_at', 'updated_at',
'platform_instance' 'platform_instance',
] ],
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform channels retrieved successfully.' 'message' => 'Platform channels retrieved successfully.',
]); ]);
} }
public function test_store_creates_platform_channel_successfully(): void public function test_store_creates_platform_channel_successfully(): void
{ {
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
// Create a platform account for this instance first // Create a platform account for this instance first
PlatformAccount::factory()->create([ PlatformAccount::factory()->create([
'instance_url' => $instance->url, 'instance_url' => $instance->url,
'is_active' => true 'is_active' => true,
]); ]);
$data = [ $data = [
@ -76,11 +76,11 @@ public function test_store_creates_platform_channel_successfully(): void
'is_active', 'is_active',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform channel created successfully and linked to platform account!' 'message' => 'Platform channel created successfully and linked to platform account!',
]); ]);
$this->assertDatabaseHas('platform_channels', [ $this->assertDatabaseHas('platform_channels', [
@ -102,7 +102,7 @@ public function test_store_validates_platform_instance_exists(): void
{ {
$data = [ $data = [
'platform_instance_id' => 999, 'platform_instance_id' => 999,
'name' => 'Test Channel' 'name' => 'Test Channel',
]; ];
$response = $this->postJson('/api/v1/platform-channels', $data); $response = $this->postJson('/api/v1/platform-channels', $data);
@ -132,8 +132,8 @@ public function test_show_returns_platform_channel_successfully(): void
'is_active', 'is_active',
'created_at', 'created_at',
'updated_at', 'updated_at',
'platform_instance' 'platform_instance',
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
@ -141,7 +141,7 @@ public function test_show_returns_platform_channel_successfully(): void
'data' => [ 'data' => [
'id' => $channel->id, 'id' => $channel->id,
'name' => $channel->name, 'name' => $channel->name,
] ],
]); ]);
} }
@ -154,7 +154,7 @@ public function test_update_modifies_platform_channel_successfully(): void
'name' => 'Updated Channel', 'name' => 'Updated Channel',
'display_name' => 'Updated Display Name', 'display_name' => 'Updated Display Name',
'description' => 'Updated description', 'description' => 'Updated description',
'is_active' => false 'is_active' => false,
]; ];
$response = $this->putJson("/api/v1/platform-channels/{$channel->id}", $updateData); $response = $this->putJson("/api/v1/platform-channels/{$channel->id}", $updateData);
@ -162,7 +162,7 @@ public function test_update_modifies_platform_channel_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform channel updated successfully!' 'message' => 'Platform channel updated successfully!',
]); ]);
$this->assertDatabaseHas('platform_channels', [ $this->assertDatabaseHas('platform_channels', [
@ -183,11 +183,11 @@ public function test_destroy_deletes_platform_channel_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform channel deleted successfully!' 'message' => 'Platform channel deleted successfully!',
]); ]);
$this->assertDatabaseMissing('platform_channels', [ $this->assertDatabaseMissing('platform_channels', [
'id' => $channel->id 'id' => $channel->id,
]); ]);
} }
@ -196,7 +196,7 @@ public function test_toggle_activates_inactive_channel(): void
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'is_active' => false 'is_active' => false,
]); ]);
$response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle"); $response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle");
@ -204,12 +204,12 @@ public function test_toggle_activates_inactive_channel(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform channel activated successfully!' 'message' => 'Platform channel activated successfully!',
]); ]);
$this->assertDatabaseHas('platform_channels', [ $this->assertDatabaseHas('platform_channels', [
'id' => $channel->id, 'id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -218,7 +218,7 @@ public function test_toggle_deactivates_active_channel(): void
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'is_active' => true 'is_active' => true,
]); ]);
$response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle"); $response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle");
@ -226,12 +226,12 @@ public function test_toggle_deactivates_active_channel(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Platform channel deactivated successfully!' 'message' => 'Platform channel deactivated successfully!',
]); ]);
$this->assertDatabaseHas('platform_channels', [ $this->assertDatabaseHas('platform_channels', [
'id' => $channel->id, 'id' => $channel->id,
'is_active' => false 'is_active' => false,
]); ]);
} }
} }

View file

@ -18,18 +18,18 @@ public function test_index_returns_successful_response(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
// Create unique feeds and channels for this test // Create unique feeds and channels for this test
$feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]); $feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]);
$channels = PlatformChannel::factory()->count(3)->create([ $channels = PlatformChannel::factory()->count(3)->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
foreach ($feeds as $index => $feed) { foreach ($feeds as $index => $feed) {
Route::factory()->create([ Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channels[$index]->id 'platform_channel_id' => $channels[$index]->id,
]); ]);
} }
@ -47,12 +47,12 @@ public function test_index_returns_successful_response(): void
'priority', 'priority',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configurations retrieved successfully.' 'message' => 'Routing configurations retrieved successfully.',
]); ]);
} }
@ -63,14 +63,14 @@ public function test_store_creates_routing_configuration_successfully(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$data = [ $data = [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 5 'priority' => 5,
]; ];
$response = $this->postJson('/api/v1/routing', $data); $response = $this->postJson('/api/v1/routing', $data);
@ -86,11 +86,11 @@ public function test_store_creates_routing_configuration_successfully(): void
'priority', 'priority',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configuration created successfully!' 'message' => 'Routing configuration created successfully!',
]); ]);
$this->assertDatabaseHas('routes', [ $this->assertDatabaseHas('routes', [
@ -115,12 +115,12 @@ public function test_store_validates_feed_exists(): void
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$data = [ $data = [
'feed_id' => 999, 'feed_id' => 999,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]; ];
$response = $this->postJson('/api/v1/routing', $data); $response = $this->postJson('/api/v1/routing', $data);
@ -136,7 +136,7 @@ public function test_store_validates_platform_channel_exists(): void
$data = [ $data = [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => 999 'platform_channel_id' => 999,
]; ];
$response = $this->postJson('/api/v1/routing', $data); $response = $this->postJson('/api/v1/routing', $data);
@ -152,12 +152,12 @@ public function test_show_returns_routing_configuration_successfully(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]); ]);
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}"); $response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
@ -173,11 +173,11 @@ public function test_show_returns_routing_configuration_successfully(): void
'priority', 'priority',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configuration retrieved successfully.' 'message' => 'Routing configuration retrieved successfully.',
]); ]);
} }
@ -188,7 +188,7 @@ public function test_show_returns_404_for_nonexistent_routing(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}"); $response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
@ -196,7 +196,7 @@ public function test_show_returns_404_for_nonexistent_routing(): void
$response->assertStatus(404) $response->assertStatus(404)
->assertJson([ ->assertJson([
'success' => false, 'success' => false,
'message' => 'Routing configuration not found.' 'message' => 'Routing configuration not found.',
]); ]);
} }
@ -207,19 +207,19 @@ public function test_update_modifies_routing_configuration_successfully(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 1 'priority' => 1,
]); ]);
$updateData = [ $updateData = [
'is_active' => false, 'is_active' => false,
'priority' => 10 'priority' => 10,
]; ];
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", $updateData); $response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", $updateData);
@ -227,7 +227,7 @@ public function test_update_modifies_routing_configuration_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configuration updated successfully!' 'message' => 'Routing configuration updated successfully!',
]); ]);
$this->assertDatabaseHas('routes', [ $this->assertDatabaseHas('routes', [
@ -245,17 +245,17 @@ public function test_update_returns_404_for_nonexistent_routing(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [ $response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
'is_active' => false 'is_active' => false,
]); ]);
$response->assertStatus(404) $response->assertStatus(404)
->assertJson([ ->assertJson([
'success' => false, 'success' => false,
'message' => 'Routing configuration not found.' 'message' => 'Routing configuration not found.',
]); ]);
} }
@ -266,12 +266,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]); ]);
$response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}"); $response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}");
@ -279,12 +279,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configuration deleted successfully!' 'message' => 'Routing configuration deleted successfully!',
]); ]);
$this->assertDatabaseMissing('routes', [ $this->assertDatabaseMissing('routes', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]); ]);
} }
@ -295,7 +295,7 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}"); $response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}");
@ -303,7 +303,7 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void
$response->assertStatus(404) $response->assertStatus(404)
->assertJson([ ->assertJson([
'success' => false, 'success' => false,
'message' => 'Routing configuration not found.' 'message' => 'Routing configuration not found.',
]); ]);
} }
@ -314,13 +314,13 @@ public function test_toggle_activates_inactive_routing(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => false 'is_active' => false,
]); ]);
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle"); $response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
@ -328,13 +328,13 @@ public function test_toggle_activates_inactive_routing(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configuration activated successfully!' 'message' => 'Routing configuration activated successfully!',
]); ]);
$this->assertDatabaseHas('routes', [ $this->assertDatabaseHas('routes', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -345,13 +345,13 @@ public function test_toggle_deactivates_active_routing(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle"); $response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
@ -359,13 +359,13 @@ public function test_toggle_deactivates_active_routing(): void
$response->assertStatus(200) $response->assertStatus(200)
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Routing configuration deactivated successfully!' 'message' => 'Routing configuration deactivated successfully!',
]); ]);
$this->assertDatabaseHas('routes', [ $this->assertDatabaseHas('routes', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => false 'is_active' => false,
]); ]);
} }
@ -376,7 +376,7 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id 'language_id' => $language->id,
]); ]);
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle"); $response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
@ -384,7 +384,7 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void
$response->assertStatus(404) $response->assertStatus(404)
->assertJson([ ->assertJson([
'success' => false, 'success' => false,
'message' => 'Routing configuration not found.' 'message' => 'Routing configuration not found.',
]); ]);
} }
} }

View file

@ -22,11 +22,11 @@ public function test_index_returns_current_settings(): void
'publishing_approvals_enabled', 'publishing_approvals_enabled',
'article_publishing_interval', 'article_publishing_interval',
], ],
'message' 'message',
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Settings retrieved successfully.' 'message' => 'Settings retrieved successfully.',
]); ]);
} }
@ -42,7 +42,7 @@ public function test_update_modifies_article_processing_setting(): void
'message' => 'Settings updated successfully.', 'message' => 'Settings updated successfully.',
'data' => [ 'data' => [
'article_processing_enabled' => false, 'article_processing_enabled' => false,
] ],
]); ]);
} }
@ -58,7 +58,7 @@ public function test_update_modifies_publishing_approvals_setting(): void
'message' => 'Settings updated successfully.', 'message' => 'Settings updated successfully.',
'data' => [ 'data' => [
'publishing_approvals_enabled' => true, 'publishing_approvals_enabled' => true,
] ],
]); ]);
} }
@ -72,7 +72,7 @@ public function test_update_validates_boolean_values(): void
$response->assertStatus(422) $response->assertStatus(422)
->assertJsonValidationErrors([ ->assertJsonValidationErrors([
'article_processing_enabled', 'article_processing_enabled',
'publishing_approvals_enabled' 'publishing_approvals_enabled',
]); ]);
} }
@ -88,7 +88,7 @@ public function test_update_accepts_partial_updates(): void
'success' => true, 'success' => true,
'data' => [ 'data' => [
'article_processing_enabled' => true, 'article_processing_enabled' => true,
] ],
]); ]);
// Should still have structure for all settings // Should still have structure for all settings
@ -97,7 +97,7 @@ public function test_update_accepts_partial_updates(): void
'article_processing_enabled', 'article_processing_enabled',
'publishing_approvals_enabled', 'publishing_approvals_enabled',
'article_publishing_interval', 'article_publishing_interval',
] ],
]); ]);
} }

View file

@ -18,11 +18,10 @@
use App\Models\Article; use App\Models\Article;
use App\Models\Feed; use App\Models\Feed;
use App\Models\Log; use App\Models\Log;
use App\Models\Setting;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Services\Log\LogSaver; use App\Models\Setting;
use App\Services\Article\ArticleFetcher; use App\Services\Article\ArticleFetcher;
use App\Services\Article\ValidationService; use App\Services\Log\LogSaver;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
@ -44,7 +43,7 @@ public function test_article_discovery_job_processes_successfully(): void
$feed = Feed::factory()->create(['is_active' => true]); $feed = Feed::factory()->create(['is_active' => true]);
$logSaver = app(LogSaver::class); $logSaver = app(LogSaver::class);
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
$job->handle($logSaver); $job->handle($logSaver);
// Should dispatch individual feed jobs // Should dispatch individual feed jobs
@ -57,7 +56,7 @@ public function test_article_discovery_for_feed_job_processes_feed(): void
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'url' => 'https://example.com/feed', 'url' => 'https://example.com/feed',
'is_active' => true 'is_active' => true,
]); ]);
// Mock the ArticleFetcher service in the container // Mock the ArticleFetcher service in the container
@ -94,10 +93,9 @@ public function test_sync_channel_posts_job_processes_successfully(): void
$this->assertTrue(true); $this->assertTrue(true);
} }
public function test_publish_next_article_job_has_correct_configuration(): void public function test_publish_next_article_job_has_correct_configuration(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$this->assertEquals('publishing', $job->queue); $this->assertEquals('publishing', $job->queue);
$this->assertInstanceOf(PublishNextArticleJob::class, $job); $this->assertInstanceOf(PublishNextArticleJob::class, $job);
@ -164,12 +162,12 @@ public function test_exception_logged_event_is_dispatched(): void
$log = Log::factory()->create([ $log = Log::factory()->create([
'level' => 'error', 'level' => 'error',
'message' => 'Test error', 'message' => 'Test error',
'context' => json_encode(['key' => 'value']) 'context' => json_encode(['key' => 'value']),
]); ]);
event(new ExceptionLogged($log)); event(new ExceptionLogged($log));
Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) use ($log) { Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) {
return $event->log->message === 'Test error'; return $event->log->message === 'Test error';
}); });
} }
@ -185,7 +183,7 @@ public function test_validate_article_listener_processes_new_article(): void
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'pending', 'approval_status' => 'pending',
]); ]);
// Mock ArticleFetcher to return valid article data // Mock ArticleFetcher to return valid article data
$mockFetcher = \Mockery::mock(\App\Services\Article\ArticleFetcher::class); $mockFetcher = \Mockery::mock(\App\Services\Article\ArticleFetcher::class);
@ -195,7 +193,7 @@ public function test_validate_article_listener_processes_new_article(): void
->andReturn([ ->andReturn([
'title' => 'Belgian News', 'title' => 'Belgian News',
'description' => 'News from Belgium', 'description' => 'News from Belgium',
'full_article' => 'This is a test article about Belgium and Belgian politics.' 'full_article' => 'This is a test article about Belgium and Belgian politics.',
]); ]);
$listener = app(ValidateArticleListener::class); $listener = app(ValidateArticleListener::class);
@ -248,10 +246,10 @@ public function test_log_exception_to_database_listener_creates_log(): void
$log = Log::factory()->create([ $log = Log::factory()->create([
'level' => 'error', 'level' => 'error',
'message' => 'Test exception message', 'message' => 'Test exception message',
'context' => json_encode(['error' => 'details']) 'context' => json_encode(['error' => 'details']),
]); ]);
$listener = new LogExceptionToDatabase(); $listener = new LogExceptionToDatabase;
$exception = new \Exception('Test exception message'); $exception = new \Exception('Test exception message');
$event = new ExceptionOccurred($exception, \App\Enums\LogLevelEnum::ERROR, 'Test exception message'); $event = new ExceptionOccurred($exception, \App\Enums\LogLevelEnum::ERROR, 'Test exception message');
@ -259,7 +257,7 @@ public function test_log_exception_to_database_listener_creates_log(): void
$this->assertDatabaseHas('logs', [ $this->assertDatabaseHas('logs', [
'level' => 'error', 'level' => 'error',
'message' => 'Test exception message' 'message' => 'Test exception message',
]); ]);
$savedLog = Log::where('message', 'Test exception message')->first(); $savedLog = Log::where('message', 'Test exception message')->first();
@ -287,7 +285,7 @@ public function test_event_listener_registration_works(): void
public function test_job_retry_configuration(): void public function test_job_retry_configuration(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Test that job has unique configuration // Test that job has unique configuration
$this->assertObjectHasProperty('uniqueFor', $job); $this->assertObjectHasProperty('uniqueFor', $job);
@ -300,9 +298,9 @@ public function test_job_queue_configuration(): void
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$article = Article::factory()->create(['feed_id' => $feed->id]); $article = Article::factory()->create(['feed_id' => $feed->id]);
$discoveryJob = new ArticleDiscoveryJob(); $discoveryJob = new ArticleDiscoveryJob;
$feedJob = new ArticleDiscoveryForFeedJob($feed); $feedJob = new ArticleDiscoveryForFeedJob($feed);
$publishJob = new PublishNextArticleJob(); $publishJob = new PublishNextArticleJob;
$syncJob = new SyncChannelPostsJob($channel); $syncJob = new SyncChannelPostsJob($channel);
// Test queue assignments // Test queue assignments

View file

@ -2,7 +2,7 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Events\ArticleReadyToPublish; use App\Events\ArticleApproved;
use App\Events\NewArticleFetched; use App\Events\NewArticleFetched;
use App\Listeners\ValidateArticleListener; use App\Listeners\ValidateArticleListener;
use App\Models\Article; use App\Models\Article;
@ -20,11 +20,11 @@ class ValidateArticleListenerTest extends TestCase
public function test_listener_validates_article_and_dispatches_ready_to_publish_event(): void public function test_listener_validates_article_and_dispatches_ready_to_publish_event(): void
{ {
Event::fake([ArticleReadyToPublish::class]); Event::fake([ArticleApproved::class]);
// Mock HTTP requests // Mock HTTP requests
Http::fake([ Http::fake([
'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200) 'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200),
]); ]);
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
@ -42,17 +42,17 @@ public function test_listener_validates_article_and_dispatches_ready_to_publish_
$article->refresh(); $article->refresh();
if ($article->isValid()) { if ($article->isValid()) {
Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) { Event::assertDispatched(ArticleApproved::class, function (ArticleApproved $event) use ($article) {
return $event->article->id === $article->id; return $event->article->id === $article->id;
}); });
} else { } else {
Event::assertNotDispatched(ArticleReadyToPublish::class); Event::assertNotDispatched(ArticleApproved::class);
} }
} }
public function test_listener_skips_already_validated_articles(): void public function test_listener_skips_already_validated_articles(): void
{ {
Event::fake([ArticleReadyToPublish::class]); Event::fake([ArticleApproved::class]);
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
@ -66,12 +66,12 @@ public function test_listener_skips_already_validated_articles(): void
$listener->handle($event); $listener->handle($event);
Event::assertNotDispatched(ArticleReadyToPublish::class); Event::assertNotDispatched(ArticleApproved::class);
} }
public function test_listener_skips_articles_with_existing_publication(): void public function test_listener_skips_articles_with_existing_publication(): void
{ {
Event::fake([ArticleReadyToPublish::class]); Event::fake([ArticleApproved::class]);
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
@ -93,16 +93,16 @@ public function test_listener_skips_articles_with_existing_publication(): void
$listener->handle($event); $listener->handle($event);
Event::assertNotDispatched(ArticleReadyToPublish::class); Event::assertNotDispatched(ArticleApproved::class);
} }
public function test_listener_calls_validation_service(): void public function test_listener_calls_validation_service(): void
{ {
Event::fake([ArticleReadyToPublish::class]); Event::fake([ArticleApproved::class]);
// Mock HTTP requests // Mock HTTP requests
Http::fake([ Http::fake([
'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200) 'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200),
]); ]);
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();

View file

@ -3,8 +3,8 @@
namespace Tests; namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\Http;
use Mockery; use Mockery;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
@ -14,13 +14,13 @@ abstract class TestCase extends BaseTestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
// Clean up any existing Mockery instances before each test // Clean up any existing Mockery instances before each test
if (class_exists(Mockery::class)) { if (class_exists(Mockery::class)) {
Mockery::close(); Mockery::close();
Mockery::globalHelpers(); Mockery::globalHelpers();
} }
// Prevent any external HTTP requests during tests unless explicitly faked in a test // Prevent any external HTTP requests during tests unless explicitly faked in a test
Http::preventStrayRequests(); Http::preventStrayRequests();
} }
@ -29,15 +29,15 @@ protected function tearDown(): void
{ {
// Clear HTTP fakes between tests to prevent interference // Clear HTTP fakes between tests to prevent interference
Http::clearResolvedInstances(); Http::clearResolvedInstances();
// Clear all facade instances to prevent interference // Clear all facade instances to prevent interference
Facade::clearResolvedInstances(); Facade::clearResolvedInstances();
// Ensure Mockery is properly closed to prevent facade interference // Ensure Mockery is properly closed to prevent facade interference
if (class_exists(Mockery::class)) { if (class_exists(Mockery::class)) {
Mockery::close(); Mockery::close();
} }
parent::tearDown(); parent::tearDown();
} }
} }

View file

@ -10,7 +10,7 @@ trait CreatesArticleFetcher
{ {
protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetcher protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetcher
{ {
if (!$logSaver) { if (! $logSaver) {
$logSaver = Mockery::mock(LogSaver::class); $logSaver = Mockery::mock(LogSaver::class);
$logSaver->shouldReceive('info')->zeroOrMoreTimes(); $logSaver->shouldReceive('info')->zeroOrMoreTimes();
$logSaver->shouldReceive('warning')->zeroOrMoreTimes(); $logSaver->shouldReceive('warning')->zeroOrMoreTimes();
@ -21,6 +21,7 @@ protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetc
return new ArticleFetcher($logSaver); return new ArticleFetcher($logSaver);
} }
/** @return array{ArticleFetcher, \Mockery\MockInterface} */
protected function createArticleFetcherWithMockedLogSaver(): array protected function createArticleFetcherWithMockedLogSaver(): array
{ {
$logSaver = Mockery::mock(LogSaver::class); $logSaver = Mockery::mock(LogSaver::class);
@ -28,9 +29,9 @@ protected function createArticleFetcherWithMockedLogSaver(): array
$logSaver->shouldReceive('warning')->zeroOrMoreTimes(); $logSaver->shouldReceive('warning')->zeroOrMoreTimes();
$logSaver->shouldReceive('error')->zeroOrMoreTimes(); $logSaver->shouldReceive('error')->zeroOrMoreTimes();
$logSaver->shouldReceive('debug')->zeroOrMoreTimes(); $logSaver->shouldReceive('debug')->zeroOrMoreTimes();
$articleFetcher = new ArticleFetcher($logSaver); $articleFetcher = new ArticleFetcher($logSaver);
return [$articleFetcher, $logSaver]; return [$articleFetcher, $logSaver];
} }
} }

View file

@ -19,7 +19,7 @@ class CreateChannelActionTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->action = new CreateChannelAction(); $this->action = new CreateChannelAction;
} }
public function test_creates_channel_and_attaches_account(): void public function test_creates_channel_and_attaches_account(): void
@ -44,7 +44,7 @@ public function test_creates_channel_and_attaches_account(): void
// Verify account is attached // Verify account is attached
$this->assertTrue($channel->platformAccounts->contains($account)); $this->assertTrue($channel->platformAccounts->contains($account));
$this->assertEquals(1, $channel->platformAccounts->first()->pivot->priority); $this->assertEquals(1, $channel->platformAccounts->first()->pivot->priority); // @phpstan-ignore property.notFound
} }
public function test_creates_channel_without_language(): void public function test_creates_channel_without_language(): void
@ -57,7 +57,7 @@ public function test_creates_channel_without_language(): void
$channel = $this->action->execute('test_community', $instance->id); $channel = $this->action->execute('test_community', $instance->id);
$this->assertNull($channel->language_id); $this->assertNull($channel->language_id); // @phpstan-ignore method.impossibleType
} }
public function test_fails_when_no_active_accounts(): void public function test_fails_when_no_active_accounts(): void

View file

@ -17,7 +17,7 @@ class CreateFeedActionTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->action = new CreateFeedAction(); $this->action = new CreateFeedAction;
} }
public function test_creates_vrt_feed_with_correct_url(): void public function test_creates_vrt_feed_with_correct_url(): void
@ -45,7 +45,7 @@ public function test_creates_belga_feed_with_correct_url(): void
$this->assertEquals('https://www.belganewsagency.eu/', $feed->url); $this->assertEquals('https://www.belganewsagency.eu/', $feed->url);
$this->assertEquals('website', $feed->type); $this->assertEquals('website', $feed->type);
$this->assertEquals('belga', $feed->provider); $this->assertEquals('belga', $feed->provider);
$this->assertNull($feed->description); $this->assertNull($feed->description); // @phpstan-ignore method.impossibleType
} }
public function test_creates_guardian_feed_with_correct_url(): void public function test_creates_guardian_feed_with_correct_url(): void
@ -57,7 +57,7 @@ public function test_creates_guardian_feed_with_correct_url(): void
$this->assertEquals('https://www.theguardian.com/international/rss', $feed->url); $this->assertEquals('https://www.theguardian.com/international/rss', $feed->url);
$this->assertEquals('rss', $feed->type); $this->assertEquals('rss', $feed->type);
$this->assertEquals('guardian', $feed->provider); $this->assertEquals('guardian', $feed->provider);
$this->assertNull($feed->description); $this->assertNull($feed->description); // @phpstan-ignore method.impossibleType
} }
public function test_creates_vrt_feed_with_dutch_language(): void public function test_creates_vrt_feed_with_dutch_language(): void

View file

@ -17,6 +17,8 @@ class CreatePlatformAccountActionTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
private CreatePlatformAccountAction $action; private CreatePlatformAccountAction $action;
/** @var LemmyAuthService&\Mockery\MockInterface */
private LemmyAuthService $lemmyAuthService; private LemmyAuthService $lemmyAuthService;
protected function setUp(): void protected function setUp(): void
@ -56,7 +58,6 @@ public function test_creates_platform_account_with_new_instance(): void
$this->assertEquals('Test User', $account->settings['display_name']); $this->assertEquals('Test User', $account->settings['display_name']);
$this->assertEquals('A test bio', $account->settings['description']); $this->assertEquals('A test bio', $account->settings['description']);
$this->assertEquals('test-jwt-token', $account->settings['api_token']); $this->assertEquals('test-jwt-token', $account->settings['api_token']);
$this->assertDatabaseHas('platform_instances', [ $this->assertDatabaseHas('platform_instances', [
'url' => 'https://lemmy.world', 'url' => 'https://lemmy.world',
'platform' => 'lemmy', 'platform' => 'lemmy',

View file

@ -19,7 +19,7 @@ class CreateRouteActionTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->action = new CreateRouteAction(); $this->action = new CreateRouteAction;
} }
public function test_creates_route_with_defaults(): void public function test_creates_route_with_defaults(): void
@ -69,7 +69,7 @@ public function test_returns_existing_route_for_duplicate_feed_channel_pair(): v
$first = $this->action->execute($feed->id, $channel->id, 10); $first = $this->action->execute($feed->id, $channel->id, 10);
$second = $this->action->execute($feed->id, $channel->id, 99); $second = $this->action->execute($feed->id, $channel->id, 99);
$this->assertEquals($first->id, $second->id); $this->assertEquals($first->id, $second->id); // @phpstan-ignore property.notFound, property.notFound
$this->assertEquals(10, $second->priority); $this->assertEquals(10, $second->priority);
$this->assertDatabaseCount('routes', 1); $this->assertDatabaseCount('routes', 1);
} }

View file

@ -28,7 +28,7 @@ public function test_to_array_returns_all_enum_values(): void
public function test_enum_cases_exist(): void public function test_enum_cases_exist(): void
{ {
$cases = LogLevelEnum::cases(); $cases = LogLevelEnum::cases();
$this->assertCount(5, $cases); $this->assertCount(5, $cases);
$this->assertContains(LogLevelEnum::DEBUG, $cases); $this->assertContains(LogLevelEnum::DEBUG, $cases);
$this->assertContains(LogLevelEnum::INFO, $cases); $this->assertContains(LogLevelEnum::INFO, $cases);
@ -83,8 +83,8 @@ public function test_enum_can_be_compared(): void
$debug2 = LogLevelEnum::DEBUG; $debug2 = LogLevelEnum::DEBUG;
$info = LogLevelEnum::INFO; $info = LogLevelEnum::INFO;
$this->assertTrue($debug1 === $debug2); $this->assertTrue($debug1 === $debug2); // @phpstan-ignore identical.alwaysTrue
$this->assertFalse($debug1 === $info); $this->assertFalse($debug1 === $info); // @phpstan-ignore identical.alwaysFalse, method.impossibleType
} }
public function test_enum_can_be_used_in_match_expression(): void public function test_enum_can_be_used_in_match_expression(): void
@ -105,4 +105,4 @@ public function test_enum_can_be_used_in_match_expression(): void
$this->assertEquals('Error message', $getMessage(LogLevelEnum::ERROR)); $this->assertEquals('Error message', $getMessage(LogLevelEnum::ERROR));
$this->assertEquals('Critical message', $getMessage(LogLevelEnum::CRITICAL)); $this->assertEquals('Critical message', $getMessage(LogLevelEnum::CRITICAL));
} }
} }

View file

@ -15,7 +15,7 @@ public function test_enum_cases_have_correct_values(): void
public function test_enum_cases_exist(): void public function test_enum_cases_exist(): void
{ {
$cases = PlatformEnum::cases(); $cases = PlatformEnum::cases();
$this->assertCount(1, $cases); $this->assertCount(1, $cases);
$this->assertContains(PlatformEnum::LEMMY, $cases); $this->assertContains(PlatformEnum::LEMMY, $cases);
} }
@ -54,7 +54,7 @@ public function test_enum_can_be_compared(): void
$lemmy1 = PlatformEnum::LEMMY; $lemmy1 = PlatformEnum::LEMMY;
$lemmy2 = PlatformEnum::LEMMY; $lemmy2 = PlatformEnum::LEMMY;
$this->assertTrue($lemmy1 === $lemmy2); $this->assertTrue($lemmy1 === $lemmy2); // @phpstan-ignore identical.alwaysTrue
} }
public function test_enum_can_be_used_in_match_expression(): void public function test_enum_can_be_used_in_match_expression(): void
@ -86,4 +86,4 @@ public function test_enum_value_is_string_backed(): void
{ {
$this->assertIsString(PlatformEnum::LEMMY->value); $this->assertIsString(PlatformEnum::LEMMY->value);
} }
} }

View file

@ -18,10 +18,10 @@ public function test_exception_constructs_with_correct_message(): void
// Arrange // Arrange
$englishLang = Language::factory()->create(['short_code' => 'en', 'name' => 'English']); $englishLang = Language::factory()->create(['short_code' => 'en', 'name' => 'English']);
$frenchLang = Language::factory()->create(['short_code' => 'fr', 'name' => 'French']); $frenchLang = Language::factory()->create(['short_code' => 'fr', 'name' => 'French']);
$feed = new Feed(['name' => 'Test Feed']); $feed = new Feed(['name' => 'Test Feed']);
$feed->setRelation('language', $englishLang); $feed->setRelation('language', $englishLang);
$channel = new PlatformChannel(['name' => 'Test Channel']); $channel = new PlatformChannel(['name' => 'Test Channel']);
$channel->setRelation('language', $frenchLang); $channel->setRelation('language', $frenchLang);
@ -41,10 +41,10 @@ public function test_exception_extends_routing_exception(): void
// Arrange // Arrange
$englishLang = Language::factory()->create(['short_code' => 'en']); $englishLang = Language::factory()->create(['short_code' => 'en']);
$frenchLang = Language::factory()->create(['short_code' => 'fr']); $frenchLang = Language::factory()->create(['short_code' => 'fr']);
$feed = new Feed(['name' => 'Test Feed']); $feed = new Feed(['name' => 'Test Feed']);
$feed->setRelation('language', $englishLang); $feed->setRelation('language', $englishLang);
$channel = new PlatformChannel(['name' => 'Test Channel']); $channel = new PlatformChannel(['name' => 'Test Channel']);
$channel->setRelation('language', $frenchLang); $channel->setRelation('language', $frenchLang);
@ -60,10 +60,10 @@ public function test_exception_with_different_languages(): void
// Arrange // Arrange
$dutchLang = Language::factory()->create(['short_code' => 'nl', 'name' => 'Dutch']); $dutchLang = Language::factory()->create(['short_code' => 'nl', 'name' => 'Dutch']);
$germanLang = Language::factory()->create(['short_code' => 'de', 'name' => 'German']); $germanLang = Language::factory()->create(['short_code' => 'de', 'name' => 'German']);
$feed = new Feed(['name' => 'Dutch News']); $feed = new Feed(['name' => 'Dutch News']);
$feed->setRelation('language', $dutchLang); $feed->setRelation('language', $dutchLang);
$channel = new PlatformChannel(['name' => 'German Channel']); $channel = new PlatformChannel(['name' => 'German Channel']);
$channel->setRelation('language', $germanLang); $channel->setRelation('language', $germanLang);
@ -82,10 +82,10 @@ public function test_exception_message_contains_all_required_elements(): void
// Arrange // Arrange
$frenchLang = Language::factory()->create(['short_code' => 'fr', 'name' => 'French']); $frenchLang = Language::factory()->create(['short_code' => 'fr', 'name' => 'French']);
$spanishLang = Language::factory()->create(['short_code' => 'es', 'name' => 'Spanish']); $spanishLang = Language::factory()->create(['short_code' => 'es', 'name' => 'Spanish']);
$feed = new Feed(['name' => 'French Feed']); $feed = new Feed(['name' => 'French Feed']);
$feed->setRelation('language', $frenchLang); $feed->setRelation('language', $frenchLang);
$channel = new PlatformChannel(['name' => 'Spanish Channel']); $channel = new PlatformChannel(['name' => 'Spanish Channel']);
$channel->setRelation('language', $spanishLang); $channel->setRelation('language', $spanishLang);
@ -105,7 +105,7 @@ public function test_exception_with_null_languages(): void
// Arrange // Arrange
$feed = new Feed(['name' => 'No Lang Feed']); $feed = new Feed(['name' => 'No Lang Feed']);
$feed->setRelation('language', null); $feed->setRelation('language', null);
$channel = new PlatformChannel(['name' => 'No Lang Channel']); $channel = new PlatformChannel(['name' => 'No Lang Channel']);
$channel->setRelation('language', null); $channel->setRelation('language', null);
@ -124,10 +124,10 @@ public function test_exception_with_special_characters_in_names(): void
// Arrange // Arrange
$englishLang = Language::factory()->create(['short_code' => 'en']); $englishLang = Language::factory()->create(['short_code' => 'en']);
$frenchLang = Language::factory()->create(['short_code' => 'fr']); $frenchLang = Language::factory()->create(['short_code' => 'fr']);
$feed = new Feed(['name' => 'Feed with "quotes" & symbols']); $feed = new Feed(['name' => 'Feed with "quotes" & symbols']);
$feed->setRelation('language', $englishLang); $feed->setRelation('language', $englishLang);
$channel = new PlatformChannel(['name' => 'Channel with <tags>']); $channel = new PlatformChannel(['name' => 'Channel with <tags>']);
$channel->setRelation('language', $frenchLang); $channel->setRelation('language', $frenchLang);
@ -146,17 +146,17 @@ public function test_exception_is_throwable(): void
// Arrange // Arrange
$englishLang = Language::factory()->create(['short_code' => 'en']); $englishLang = Language::factory()->create(['short_code' => 'en']);
$frenchLang = Language::factory()->create(['short_code' => 'fr']); $frenchLang = Language::factory()->create(['short_code' => 'fr']);
$feed = new Feed(['name' => 'Test Feed']); $feed = new Feed(['name' => 'Test Feed']);
$feed->setRelation('language', $englishLang); $feed->setRelation('language', $englishLang);
$channel = new PlatformChannel(['name' => 'Test Channel']); $channel = new PlatformChannel(['name' => 'Test Channel']);
$channel->setRelation('language', $frenchLang); $channel->setRelation('language', $frenchLang);
// Act & Assert // Act & Assert
$this->expectException(RoutingMismatchException::class); $this->expectException(RoutingMismatchException::class);
$this->expectExceptionMessage('Language mismatch'); $this->expectExceptionMessage('Language mismatch');
throw new RoutingMismatchException($feed, $channel); throw new RoutingMismatchException($feed, $channel);
} }
} }

View file

@ -3,11 +3,11 @@
namespace Tests\Unit\Facades; namespace Tests\Unit\Facades;
use App\Enums\LogLevelEnum; use App\Enums\LogLevelEnum;
use App\Enums\PlatformEnum;
use App\Facades\LogSaver; use App\Facades\LogSaver;
use App\Models\Log; use App\Models\Log;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\PlatformInstance; use App\Models\PlatformInstance;
use App\Enums\PlatformEnum;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -20,7 +20,7 @@ public function test_facade_accessor_returns_correct_service(): void
$reflection = new \ReflectionClass(LogSaver::class); $reflection = new \ReflectionClass(LogSaver::class);
$method = $reflection->getMethod('getFacadeAccessor'); $method = $reflection->getMethod('getFacadeAccessor');
$method->setAccessible(true); $method->setAccessible(true);
$this->assertEquals(\App\Services\Log\LogSaver::class, $method->invoke(null)); $this->assertEquals(\App\Services\Log\LogSaver::class, $method->invoke(null));
} }
@ -92,12 +92,12 @@ public function test_facade_works_with_channel(): void
{ {
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://facade.test.com' 'url' => 'https://facade.test.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'Facade Test Channel', 'name' => 'Facade Test Channel',
'platform_instance_id' => $platformInstance->id 'platform_instance_id' => $platformInstance->id,
]); ]);
$message = 'Facade channel test'; $message = 'Facade channel test';
@ -125,11 +125,11 @@ public function test_facade_static_calls_resolve_to_service_instance(): void
LogSaver::error('Test message 2'); LogSaver::error('Test message 2');
$this->assertDatabaseCount('logs', 2); $this->assertDatabaseCount('logs', 2);
$logs = Log::orderBy('id')->get(); $logs = Log::orderBy('id')->get();
$this->assertEquals('Test message 1', $logs[0]->message); $this->assertEquals('Test message 1', $logs[0]->message);
$this->assertEquals('Test message 2', $logs[1]->message); $this->assertEquals('Test message 2', $logs[1]->message);
$this->assertEquals(LogLevelEnum::INFO, $logs[0]->level); $this->assertEquals(LogLevelEnum::INFO, $logs[0]->level);
$this->assertEquals(LogLevelEnum::ERROR, $logs[1]->level); $this->assertEquals(LogLevelEnum::ERROR, $logs[1]->level);
} }
} }

View file

@ -25,7 +25,7 @@ public function test_constructor_sets_correct_queue(): void
{ {
$feed = Feed::factory()->make(); $feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed); $job = new ArticleDiscoveryForFeedJob($feed);
$this->assertEquals('feed-discovery', $job->queue); $this->assertEquals('feed-discovery', $job->queue);
} }
@ -33,7 +33,7 @@ public function test_job_implements_should_queue(): void
{ {
$feed = Feed::factory()->make(); $feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed); $job = new ArticleDiscoveryForFeedJob($feed);
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
} }
@ -41,7 +41,7 @@ public function test_job_uses_queueable_trait(): void
{ {
$feed = Feed::factory()->make(); $feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed); $job = new ArticleDiscoveryForFeedJob($feed);
$this->assertContains( $this->assertContains(
\Illuminate\Foundation\Queue\Queueable::class, \Illuminate\Foundation\Queue\Queueable::class,
class_uses($job) class_uses($job)
@ -54,7 +54,7 @@ public function test_handle_fetches_articles_and_updates_feed(): void
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed', 'url' => 'https://example.com/feed',
'last_fetched_at' => null 'last_fetched_at' => null,
]); ]);
$mockArticles = collect(['article1', 'article2']); $mockArticles = collect(['article1', 'article2']);
@ -72,15 +72,15 @@ public function test_handle_fetches_articles_and_updates_feed(): void
->with('Starting feed article fetch', null, [ ->with('Starting feed article fetch', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_name' => $feed->name, 'feed_name' => $feed->name,
'feed_url' => $feed->url 'feed_url' => $feed->url,
]) ])
->once(); ->once();
$logSaverMock->shouldReceive('info') $logSaverMock->shouldReceive('info')
->with('Feed article fetch completed', null, [ ->with('Feed article fetch completed', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_name' => $feed->name, 'feed_name' => $feed->name,
'articles_count' => 2 'articles_count' => 2,
]) ])
->once(); ->once();
@ -106,7 +106,7 @@ public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay():
$logSaverMock->shouldReceive('info') $logSaverMock->shouldReceive('info')
->times(3) // Once for each active feed ->times(3) // Once for each active feed
->with('Dispatched feed discovery job', null, Mockery::type('array')); ->with('Dispatched feed discovery job', null, Mockery::type('array'));
$this->app->instance(LogSaver::class, $logSaverMock); $this->app->instance(LogSaver::class, $logSaverMock);
// Act // Act
@ -114,7 +114,7 @@ public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay():
// Assert // Assert
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 3); Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 3);
// Verify jobs were dispatched (cannot access private $feed property in test) // Verify jobs were dispatched (cannot access private $feed property in test)
} }
@ -126,7 +126,7 @@ public function test_dispatch_for_all_active_feeds_applies_correct_delays(): voi
// Mock LogSaver // Mock LogSaver
$logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->times(2); $logSaverMock->shouldReceive('info')->times(2);
$this->app->instance(LogSaver::class, $logSaverMock); $this->app->instance(LogSaver::class, $logSaverMock);
// Act // Act
@ -134,7 +134,7 @@ public function test_dispatch_for_all_active_feeds_applies_correct_delays(): voi
// Assert // Assert
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 2); Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 2);
// Verify jobs are pushed with delays // Verify jobs are pushed with delays
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, function ($job) { Queue::assertPushed(ArticleDiscoveryForFeedJob::class, function ($job) {
return $job->delay !== null; return $job->delay !== null;
@ -157,7 +157,7 @@ public function test_feed_discovery_delay_constant_exists(): void
{ {
$reflection = new \ReflectionClass(ArticleDiscoveryForFeedJob::class); $reflection = new \ReflectionClass(ArticleDiscoveryForFeedJob::class);
$constant = $reflection->getConstant('FEED_DISCOVERY_DELAY_MINUTES'); $constant = $reflection->getConstant('FEED_DISCOVERY_DELAY_MINUTES');
$this->assertEquals(5, $constant); $this->assertEquals(5, $constant);
} }
@ -165,10 +165,10 @@ public function test_job_can_be_serialized(): void
{ {
$feed = Feed::factory()->create(['name' => 'Test Feed']); $feed = Feed::factory()->create(['name' => 'Test Feed']);
$job = new ArticleDiscoveryForFeedJob($feed); $job = new ArticleDiscoveryForFeedJob($feed);
$serialized = serialize($job); $serialized = serialize($job);
$unserialized = unserialize($serialized); $unserialized = unserialize($serialized);
$this->assertInstanceOf(ArticleDiscoveryForFeedJob::class, $unserialized); $this->assertInstanceOf(ArticleDiscoveryForFeedJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue); $this->assertEquals($job->queue, $unserialized->queue);
// Note: Cannot test feed property directly as it's private // Note: Cannot test feed property directly as it's private
@ -180,7 +180,7 @@ public function test_handle_logs_start_message_with_correct_context(): void
// Arrange // Arrange
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed' 'url' => 'https://example.com/feed',
]); ]);
$mockArticles = collect([]); $mockArticles = collect([]);
@ -197,10 +197,10 @@ public function test_handle_logs_start_message_with_correct_context(): void
->with('Starting feed article fetch', null, [ ->with('Starting feed article fetch', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_name' => 'Test Feed', 'feed_name' => 'Test Feed',
'feed_url' => 'https://example.com/feed' 'feed_url' => 'https://example.com/feed',
]) ])
->once(); ->once();
$logSaverMock->shouldReceive('info') $logSaverMock->shouldReceive('info')
->with('Feed article fetch completed', null, Mockery::type('array')) ->with('Feed article fetch completed', null, Mockery::type('array'))
->once(); ->once();
@ -219,4 +219,4 @@ protected function tearDown(): void
Mockery::close(); Mockery::close();
parent::tearDown(); parent::tearDown();
} }
} }

View file

@ -23,7 +23,7 @@ protected function setUp(): void
public function test_constructor_sets_correct_queue(): void public function test_constructor_sets_correct_queue(): void
{ {
// Act // Act
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Assert // Assert
$this->assertEquals('feed-discovery', $job->queue); $this->assertEquals('feed-discovery', $job->queue);
@ -40,7 +40,7 @@ public function test_handle_skips_when_article_processing_disabled(): void
->once() ->once()
->with('Article processing is disabled. Article discovery skipped.'); ->with('Article processing is disabled. Article discovery skipped.');
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Act // Act
$job->handle($logSaverMock); $job->handle($logSaverMock);
@ -63,7 +63,7 @@ public function test_handle_dispatches_jobs_when_article_processing_enabled(): v
->with('Article discovery jobs dispatched for all active feeds') ->with('Article discovery jobs dispatched for all active feeds')
->once(); ->once();
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Act // Act
$job->handle($logSaverMock); $job->handle($logSaverMock);
@ -85,7 +85,7 @@ public function test_handle_with_default_article_processing_enabled(): void
->with('Article discovery jobs dispatched for all active feeds') ->with('Article discovery jobs dispatched for all active feeds')
->once(); ->once();
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Act // Act
$job->handle($logSaverMock); $job->handle($logSaverMock);
@ -97,7 +97,7 @@ public function test_handle_with_default_article_processing_enabled(): void
public function test_job_implements_should_queue(): void public function test_job_implements_should_queue(): void
{ {
// Arrange // Arrange
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Assert // Assert
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
@ -106,7 +106,7 @@ public function test_job_implements_should_queue(): void
public function test_job_uses_queueable_trait(): void public function test_job_uses_queueable_trait(): void
{ {
// Arrange // Arrange
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Assert // Assert
$this->assertTrue(method_exists($job, 'onQueue')); $this->assertTrue(method_exists($job, 'onQueue'));
@ -129,7 +129,7 @@ public function test_handle_logs_appropriate_messages(): void
->with('Article discovery jobs dispatched for all active feeds') ->with('Article discovery jobs dispatched for all active feeds')
->once(); ->once();
$job = new ArticleDiscoveryJob(); $job = new ArticleDiscoveryJob;
// Act - Should not throw any exceptions // Act - Should not throw any exceptions
$job->handle($logSaverMock); $job->handle($logSaverMock);

View file

@ -25,36 +25,36 @@ protected function setUp(): void
public function test_constructor_sets_correct_queue(): void public function test_constructor_sets_correct_queue(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$this->assertEquals('publishing', $job->queue); $this->assertEquals('publishing', $job->queue);
} }
public function test_job_implements_should_queue(): void public function test_job_implements_should_queue(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
} }
public function test_job_implements_should_be_unique(): void public function test_job_implements_should_be_unique(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job);
} }
public function test_job_has_unique_for_property(): void public function test_job_has_unique_for_property(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$this->assertEquals(300, $job->uniqueFor); $this->assertEquals(300, $job->uniqueFor);
} }
public function test_job_uses_queueable_trait(): void public function test_job_uses_queueable_trait(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$this->assertContains( $this->assertContains(
\Illuminate\Foundation\Queue\Queueable::class, \Illuminate\Foundation\Queue\Queueable::class,
class_uses($job) class_uses($job)
@ -66,8 +66,8 @@ public function test_handle_returns_early_when_no_approved_articles(): void
// Arrange - No articles exist // Arrange - No articles exist
$articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock = Mockery::mock(ArticleFetcher::class);
// No expectations as handle should return early // No expectations as handle should return early
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Act // Act
$publishingServiceMock = \Mockery::mock(ArticlePublishingService::class); $publishingServiceMock = \Mockery::mock(ArticlePublishingService::class);
@ -83,16 +83,16 @@ public function test_handle_returns_early_when_no_unpublished_approved_articles(
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
// Create a publication record to mark it as already published // Create a publication record to mark it as already published
ArticlePublication::factory()->create(['article_id' => $article->id]); ArticlePublication::factory()->create(['article_id' => $article->id]);
$articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock = Mockery::mock(ArticleFetcher::class);
// No expectations as handle should return early // No expectations as handle should return early
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Act // Act
$publishingServiceMock = \Mockery::mock(ArticlePublishingService::class); $publishingServiceMock = \Mockery::mock(ArticlePublishingService::class);
@ -108,17 +108,17 @@ public function test_handle_skips_non_approved_articles(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
Article::factory()->create([ Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
Article::factory()->create([ Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'rejected' 'approval_status' => 'rejected',
]); ]);
$articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock = Mockery::mock(ArticleFetcher::class);
// No expectations as handle should return early // No expectations as handle should return early
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Act // Act
$publishingServiceMock = \Mockery::mock(ArticlePublishingService::class); $publishingServiceMock = \Mockery::mock(ArticlePublishingService::class);
@ -132,19 +132,19 @@ public function test_handle_publishes_oldest_approved_article(): void
{ {
// Arrange // Arrange
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
// Create older article first // Create older article first
$olderArticle = Article::factory()->create([ $olderArticle = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved', 'approval_status' => 'approved',
'created_at' => now()->subHours(2) 'created_at' => now()->subHours(2),
]); ]);
// Create newer article // Create newer article
$newerArticle = Article::factory()->create([ $newerArticle = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved', 'approval_status' => 'approved',
'created_at' => now()->subHour() 'created_at' => now()->subHour(),
]); ]);
$extractedData = ['title' => 'Test Article', 'content' => 'Test content']; $extractedData = ['title' => 'Test Article', 'content' => 'Test content'];
@ -169,7 +169,7 @@ public function test_handle_publishes_oldest_approved_article(): void
$extractedData $extractedData
); );
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Act // Act
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
@ -184,7 +184,7 @@ public function test_handle_throws_exception_on_publishing_failure(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = ['title' => 'Test Article']; $extractedData = ['title' => 'Test Article'];
@ -203,7 +203,7 @@ public function test_handle_throws_exception_on_publishing_failure(): void
->once() ->once()
->andThrow($publishException); ->andThrow($publishException);
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Assert // Assert
$this->expectException(PublishException::class); $this->expectException(PublishException::class);
@ -220,7 +220,7 @@ public function test_handle_logs_publishing_start(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved', 'approval_status' => 'approved',
'title' => 'Test Article Title', 'title' => 'Test Article Title',
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$extractedData = ['title' => 'Test Article']; $extractedData = ['title' => 'Test Article'];
@ -235,7 +235,7 @@ public function test_handle_logs_publishing_start(): void
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class); $publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')->once(); $publishingServiceMock->shouldReceive('publishToRoutedChannels')->once();
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Act // Act
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
@ -246,11 +246,11 @@ public function test_handle_logs_publishing_start(): void
public function test_job_can_be_serialized(): void public function test_job_can_be_serialized(): void
{ {
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$serialized = serialize($job); $serialized = serialize($job);
$unserialized = unserialize($serialized); $unserialized = unserialize($serialized);
$this->assertInstanceOf(PublishNextArticleJob::class, $unserialized); $this->assertInstanceOf(PublishNextArticleJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue); $this->assertEquals($job->queue, $unserialized->queue);
$this->assertEquals($job->uniqueFor, $unserialized->uniqueFor); $this->assertEquals($job->uniqueFor, $unserialized->uniqueFor);
@ -262,7 +262,7 @@ public function test_handle_fetches_article_data_before_publishing(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = ['title' => 'Extracted Title', 'content' => 'Extracted Content']; $extractedData = ['title' => 'Extracted Title', 'content' => 'Extracted Content'];
@ -280,7 +280,7 @@ public function test_handle_fetches_article_data_before_publishing(): void
->once() ->once()
->with(Mockery::type(Article::class), $extractedData); ->with(Mockery::type(Article::class), $extractedData);
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
// Act // Act
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
@ -310,7 +310,7 @@ public function test_handle_skips_publishing_when_last_publication_within_interv
$articleFetcherMock->shouldNotReceive('fetchArticleData'); $articleFetcherMock->shouldNotReceive('fetchArticleData');
$publishingServiceMock->shouldNotReceive('publishToRoutedChannels'); $publishingServiceMock->shouldNotReceive('publishToRoutedChannels');
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true); $this->assertTrue(true);
@ -341,7 +341,7 @@ public function test_handle_publishes_when_last_publication_beyond_interval(): v
$publishingServiceMock->shouldReceive('publishToRoutedChannels') $publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once(); ->once();
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true); $this->assertTrue(true);
@ -372,7 +372,7 @@ public function test_handle_publishes_when_interval_is_zero(): void
$publishingServiceMock->shouldReceive('publishToRoutedChannels') $publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once(); ->once();
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true); $this->assertTrue(true);
@ -403,7 +403,7 @@ public function test_handle_publishes_when_last_publication_exactly_at_interval(
$publishingServiceMock->shouldReceive('publishToRoutedChannels') $publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once(); ->once();
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true); $this->assertTrue(true);
@ -430,7 +430,7 @@ public function test_handle_publishes_when_no_previous_publications_exist(): voi
$publishingServiceMock->shouldReceive('publishToRoutedChannels') $publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once(); ->once();
$job = new PublishNextArticleJob(); $job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock); $job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true); $this->assertTrue(true);
@ -441,4 +441,4 @@ protected function tearDown(): void
Mockery::close(); Mockery::close();
parent::tearDown(); parent::tearDown();
} }
} }

View file

@ -3,12 +3,10 @@
namespace Tests\Unit\Jobs; namespace Tests\Unit\Jobs;
use App\Enums\PlatformEnum; use App\Enums\PlatformEnum;
use App\Exceptions\PlatformAuthException;
use App\Jobs\SyncChannelPostsJob; use App\Jobs\SyncChannelPostsJob;
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\PlatformInstance; use App\Models\PlatformInstance;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Services\Log\LogSaver; use App\Services\Log\LogSaver;
use Exception; use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -32,7 +30,7 @@ public function test_constructor_sets_correct_queue(): void
{ {
$channel = PlatformChannel::factory()->make(); $channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);
$this->assertEquals('sync', $job->queue); $this->assertEquals('sync', $job->queue);
} }
@ -40,7 +38,7 @@ public function test_job_implements_should_queue(): void
{ {
$channel = PlatformChannel::factory()->make(); $channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
} }
@ -48,7 +46,7 @@ public function test_job_implements_should_be_unique(): void
{ {
$channel = PlatformChannel::factory()->make(); $channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job);
} }
@ -56,7 +54,7 @@ public function test_job_uses_queueable_trait(): void
{ {
$channel = PlatformChannel::factory()->make(); $channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);
$this->assertContains( $this->assertContains(
\Illuminate\Foundation\Queue\Queueable::class, \Illuminate\Foundation\Queue\Queueable::class,
class_uses($job) class_uses($job)
@ -67,30 +65,30 @@ public function test_dispatch_for_all_active_channels_dispatches_jobs(): void
{ {
// Arrange // Arrange
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY 'platform' => PlatformEnum::LEMMY,
]); ]);
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'instance_url' => $platformInstance->url, 'instance_url' => $platformInstance->url,
'is_active' => true 'is_active' => true,
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id, 'platform_instance_id' => $platformInstance->id,
'is_active' => true 'is_active' => true,
]); ]);
// Attach account to channel with active status // Attach account to channel with active status
$channel->platformAccounts()->attach($account->id, [ $channel->platformAccounts()->attach($account->id, [
'is_active' => true, 'is_active' => true,
'created_at' => now(), 'created_at' => now(),
'updated_at' => now() 'updated_at' => now(),
]); ]);
// Mock LogSaver to avoid strict expectations // Mock LogSaver to avoid strict expectations
$logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->zeroOrMoreTimes(); $logSaverMock->shouldReceive('info')->zeroOrMoreTimes();
$this->app->instance(LogSaver::class, $logSaverMock); $this->app->instance(LogSaver::class, $logSaverMock);
// Act // Act
@ -105,12 +103,12 @@ public function test_handle_logs_start_message(): void
// Arrange // Arrange
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.example.com' 'url' => 'https://lemmy.example.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id, 'platform_instance_id' => $platformInstance->id,
'name' => 'testcommunity' 'name' => 'testcommunity',
]); ]);
// Mock LogSaver - only test that logging methods are called // Mock LogSaver - only test that logging methods are called
@ -136,13 +134,13 @@ public function test_job_can_be_serialized(): void
$platformInstance = PlatformInstance::factory()->create(); $platformInstance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id, 'platform_instance_id' => $platformInstance->id,
'name' => 'Test Channel' 'name' => 'Test Channel',
]); ]);
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);
$serialized = serialize($job); $serialized = serialize($job);
$unserialized = unserialize($serialized); $unserialized = unserialize($serialized);
$this->assertInstanceOf(SyncChannelPostsJob::class, $unserialized); $this->assertInstanceOf(SyncChannelPostsJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue); $this->assertEquals($job->queue, $unserialized->queue);
// Note: Cannot test channel property directly as it's private // Note: Cannot test channel property directly as it's private
@ -158,7 +156,7 @@ public function test_job_has_handle_method(): void
{ {
$channel = PlatformChannel::factory()->make(); $channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);
$this->assertTrue(method_exists($job, 'handle')); $this->assertTrue(method_exists($job, 'handle'));
} }
@ -167,4 +165,4 @@ protected function tearDown(): void
Mockery::close(); Mockery::close();
parent::tearDown(); parent::tearDown();
} }
} }

View file

@ -15,21 +15,22 @@ class ArticlePublicationTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['article_id', 'platform_channel_id', 'post_id', 'published_at', 'published_by', 'platform', 'publication_data']; $fillableFields = ['article_id', 'platform_channel_id', 'post_id', 'published_at', 'published_by', 'platform', 'publication_data'];
$publication = new ArticlePublication(); $publication = new ArticlePublication;
$this->assertEquals($fillableFields, $publication->getFillable()); $this->assertEquals($fillableFields, $publication->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$publication = new ArticlePublication(); $publication = new ArticlePublication;
$this->assertEquals('article_publications', $publication->getTable()); $this->assertEquals('article_publications', $publication->getTable());
} }
public function test_casts_published_at_to_datetime(): void public function test_casts_published_at_to_datetime(): void
{ {
$timestamp = now()->subHours(2); $timestamp = now()->subHours(2);
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['published_at' => $timestamp]); $publication = ArticlePublication::factory()->create(['published_at' => $timestamp]);
$this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at); $this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at);
@ -43,11 +44,12 @@ public function test_casts_publication_data_to_array(): void
'platform_response' => [ 'platform_response' => [
'id' => 123, 'id' => 123,
'status' => 'success', 'status' => 'success',
'metadata' => ['views' => 0, 'votes' => 0] 'metadata' => ['views' => 0, 'votes' => 0],
], ],
'retry_count' => 0 'retry_count' => 0,
]; ];
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => $publicationData]); $publication = ArticlePublication::factory()->create(['publication_data' => $publicationData]);
$this->assertIsArray($publication->publication_data); $this->assertIsArray($publication->publication_data);
@ -57,6 +59,7 @@ public function test_casts_publication_data_to_array(): void
public function test_belongs_to_article_relationship(): void public function test_belongs_to_article_relationship(): void
{ {
$article = Article::factory()->create(); $article = Article::factory()->create();
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['article_id' => $article->id]); $publication = ArticlePublication::factory()->create(['article_id' => $article->id]);
$this->assertInstanceOf(Article::class, $publication->article); $this->assertInstanceOf(Article::class, $publication->article);
@ -66,6 +69,7 @@ public function test_belongs_to_article_relationship(): void
public function test_publication_creation_with_factory(): void public function test_publication_creation_with_factory(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(); $publication = ArticlePublication::factory()->create();
$this->assertInstanceOf(ArticlePublication::class, $publication); $this->assertInstanceOf(ArticlePublication::class, $publication);
@ -90,7 +94,7 @@ public function test_publication_creation_with_explicit_values(): void
'published_at' => $publishedAt, 'published_at' => $publishedAt,
'published_by' => 'test_bot', 'published_by' => 'test_bot',
'platform' => 'lemmy', 'platform' => 'lemmy',
'publication_data' => $publicationData 'publication_data' => $publicationData,
]); ]);
$this->assertEquals($article->id, $publication->article_id); $this->assertEquals($article->id, $publication->article_id);
@ -104,8 +108,9 @@ public function test_publication_creation_with_explicit_values(): void
public function test_publication_factory_recently_published_state(): void public function test_publication_factory_recently_published_state(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->recentlyPublished()->create(); $publication = ArticlePublication::factory()->recentlyPublished()->create();
$this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at); $this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at);
$this->assertTrue($publication->published_at->isAfter(now()->subDay())); $this->assertTrue($publication->published_at->isAfter(now()->subDay()));
$this->assertTrue($publication->published_at->isBefore(now()->addMinute())); $this->assertTrue($publication->published_at->isBefore(now()->addMinute()));
@ -113,14 +118,15 @@ public function test_publication_factory_recently_published_state(): void
public function test_publication_update(): void public function test_publication_update(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create([ $publication = ArticlePublication::factory()->create([
'post_id' => 'original-id', 'post_id' => 'original-id',
'published_by' => 'original_user' 'published_by' => 'original_user',
]); ]);
$publication->update([ $publication->update([
'post_id' => 'updated-id', 'post_id' => 'updated-id',
'published_by' => 'updated_user' 'published_by' => 'updated_user',
]); ]);
$publication->refresh(); $publication->refresh();
@ -131,6 +137,7 @@ public function test_publication_update(): void
public function test_publication_deletion(): void public function test_publication_deletion(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(); $publication = ArticlePublication::factory()->create();
$publicationId = $publication->id; $publicationId = $publication->id;
@ -141,6 +148,7 @@ public function test_publication_deletion(): void
public function test_publication_data_can_be_empty_array(): void public function test_publication_data_can_be_empty_array(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => []]); $publication = ArticlePublication::factory()->create(['publication_data' => []]);
$this->assertIsArray($publication->publication_data); $this->assertIsArray($publication->publication_data);
@ -149,6 +157,7 @@ public function test_publication_data_can_be_empty_array(): void
public function test_publication_data_can_be_null(): void public function test_publication_data_can_be_null(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => null]); $publication = ArticlePublication::factory()->create(['publication_data' => null]);
$this->assertNull($publication->publication_data); $this->assertNull($publication->publication_data);
@ -164,21 +173,22 @@ public function test_publication_data_can_be_complex_structure(): void
'author' => [ 'author' => [
'id' => 456, 'id' => 456,
'name' => 'bot_user', 'name' => 'bot_user',
'display_name' => 'Bot User' 'display_name' => 'Bot User',
] ],
], ],
'metadata' => [ 'metadata' => [
'retry_attempts' => 1, 'retry_attempts' => 1,
'processing_time_ms' => 1250, 'processing_time_ms' => 1250,
'error_log' => [] 'error_log' => [],
], ],
'analytics' => [ 'analytics' => [
'initial_views' => 0, 'initial_views' => 0,
'initial_votes' => 0, 'initial_votes' => 0,
'engagement_tracked' => false 'engagement_tracked' => false,
] ],
]; ];
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => $complexData]); $publication = ArticlePublication::factory()->create(['publication_data' => $complexData]);
$this->assertEquals($complexData, $publication->publication_data); $this->assertEquals($complexData, $publication->publication_data);
@ -190,6 +200,7 @@ public function test_publication_data_can_be_complex_structure(): void
public function test_publication_with_specific_published_at(): void public function test_publication_with_specific_published_at(): void
{ {
$timestamp = now()->subHours(3); $timestamp = now()->subHours(3);
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['published_at' => $timestamp]); $publication = ArticlePublication::factory()->create(['published_at' => $timestamp]);
$this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at); $this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at);
@ -198,6 +209,7 @@ public function test_publication_with_specific_published_at(): void
public function test_publication_with_specific_published_by(): void public function test_publication_with_specific_published_by(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['published_by' => 'custom_bot']); $publication = ArticlePublication::factory()->create(['published_by' => 'custom_bot']);
$this->assertEquals('custom_bot', $publication->published_by); $this->assertEquals('custom_bot', $publication->published_by);
@ -205,6 +217,7 @@ public function test_publication_with_specific_published_by(): void
public function test_publication_with_specific_platform(): void public function test_publication_with_specific_platform(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['platform' => 'lemmy']); $publication = ArticlePublication::factory()->create(['platform' => 'lemmy']);
$this->assertEquals('lemmy', $publication->platform); $this->assertEquals('lemmy', $publication->platform);
@ -212,6 +225,7 @@ public function test_publication_with_specific_platform(): void
public function test_publication_timestamps(): void public function test_publication_timestamps(): void
{ {
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(); $publication = ArticlePublication::factory()->create();
$this->assertNotNull($publication->created_at); $this->assertNotNull($publication->created_at);
@ -226,16 +240,18 @@ public function test_multiple_publications_for_same_article(): void
$channel1 = PlatformChannel::factory()->create(); $channel1 = PlatformChannel::factory()->create();
$channel2 = PlatformChannel::factory()->create(); $channel2 = PlatformChannel::factory()->create();
/** @var ArticlePublication $publication1 */
$publication1 = ArticlePublication::factory()->create([ $publication1 = ArticlePublication::factory()->create([
'article_id' => $article->id, 'article_id' => $article->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'post_id' => 'post-1' 'post_id' => 'post-1',
]); ]);
/** @var ArticlePublication $publication2 */
$publication2 = ArticlePublication::factory()->create([ $publication2 = ArticlePublication::factory()->create([
'article_id' => $article->id, 'article_id' => $article->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'post_id' => 'post-2' 'post_id' => 'post-2',
]); ]);
$this->assertEquals($article->id, $publication1->article_id); $this->assertEquals($article->id, $publication1->article_id);
@ -246,7 +262,9 @@ public function test_multiple_publications_for_same_article(): void
public function test_publication_with_different_platforms(): void public function test_publication_with_different_platforms(): void
{ {
/** @var ArticlePublication $publication1 */
$publication1 = ArticlePublication::factory()->create(['platform' => 'lemmy']); $publication1 = ArticlePublication::factory()->create(['platform' => 'lemmy']);
/** @var ArticlePublication $publication2 */
$publication2 = ArticlePublication::factory()->create(['platform' => 'lemmy']); $publication2 = ArticlePublication::factory()->create(['platform' => 'lemmy']);
$this->assertEquals('lemmy', $publication1->platform); $this->assertEquals('lemmy', $publication1->platform);
@ -255,9 +273,10 @@ public function test_publication_with_different_platforms(): void
public function test_publication_post_id_variations(): void public function test_publication_post_id_variations(): void
{ {
/** @var ArticlePublication[] $publications */
$publications = [ $publications = [
ArticlePublication::factory()->create(['post_id' => 'numeric-123']), ArticlePublication::factory()->create(['post_id' => 'numeric-123']),
ArticlePublication::factory()->create(['post_id' => 'uuid-' . fake()->uuid()]), ArticlePublication::factory()->create(['post_id' => 'uuid-'.fake()->uuid()]),
ArticlePublication::factory()->create(['post_id' => 'alphanumeric_post_456']), ArticlePublication::factory()->create(['post_id' => 'alphanumeric_post_456']),
ArticlePublication::factory()->create(['post_id' => '12345']), ArticlePublication::factory()->create(['post_id' => '12345']),
]; ];
@ -275,15 +294,16 @@ public function test_publication_data_with_error_information(): void
'error' => [ 'error' => [
'code' => 403, 'code' => 403,
'message' => 'Insufficient permissions', 'message' => 'Insufficient permissions',
'details' => 'Bot account lacks posting privileges' 'details' => 'Bot account lacks posting privileges',
], ],
'retry_info' => [ 'retry_info' => [
'max_retries' => 3, 'max_retries' => 3,
'current_attempt' => 2, 'current_attempt' => 2,
'next_retry_at' => '2023-01-01T13:00:00Z' 'next_retry_at' => '2023-01-01T13:00:00Z',
] ],
]; ];
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => $errorData]); $publication = ArticlePublication::factory()->create(['publication_data' => $errorData]);
$this->assertEquals('failed', $publication->publication_data['status']); $this->assertEquals('failed', $publication->publication_data['status']);
@ -295,12 +315,13 @@ public function test_publication_relationship_with_article_data(): void
{ {
$article = Article::factory()->create([ $article = Article::factory()->create([
'title' => 'Test Article Title', 'title' => 'Test Article Title',
'description' => 'Test article description' 'description' => 'Test article description',
]); ]);
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['article_id' => $article->id]); $publication = ArticlePublication::factory()->create(['article_id' => $article->id]);
$this->assertEquals('Test Article Title', $publication->article->title); $this->assertEquals('Test Article Title', $publication->article->title);
$this->assertEquals('Test article description', $publication->article->description); $this->assertEquals('Test article description', $publication->article->description);
} }
} }

View file

@ -22,7 +22,7 @@ protected function setUp(): void
// Mock HTTP requests to prevent external calls // Mock HTTP requests to prevent external calls
Http::fake([ Http::fake([
'*' => Http::response('', 500) '*' => Http::response('', 500),
]); ]);
// Don't fake events globally - let individual tests control this // Don't fake events globally - let individual tests control this

View file

@ -17,15 +17,15 @@ class FeedTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at']; $fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at'];
$feed = new Feed(); $feed = new Feed;
$this->assertEquals($fillableFields, $feed->getFillable()); $this->assertEquals($fillableFields, $feed->getFillable());
} }
public function test_casts_settings_to_array(): void public function test_casts_settings_to_array(): void
{ {
$settings = ['key1' => 'value1', 'key2' => ['nested' => 'value']]; $settings = ['key1' => 'value1', 'key2' => ['nested' => 'value']];
$feed = Feed::factory()->create(['settings' => $settings]); $feed = Feed::factory()->create(['settings' => $settings]);
$this->assertIsArray($feed->settings); $this->assertIsArray($feed->settings);
@ -41,7 +41,7 @@ public function test_casts_is_active_to_boolean(): void
$feed->update(['is_active' => '0']); $feed->update(['is_active' => '0']);
$feed->refresh(); $feed->refresh();
$this->assertIsBool($feed->is_active); $this->assertIsBool($feed->is_active);
$this->assertFalse($feed->is_active); $this->assertFalse($feed->is_active);
} }
@ -75,7 +75,7 @@ public function test_status_attribute_never_fetched(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'is_active' => true, 'is_active' => true,
'last_fetched_at' => null 'last_fetched_at' => null,
]); ]);
$this->assertEquals('Never fetched', $feed->status); $this->assertEquals('Never fetched', $feed->status);
@ -85,7 +85,7 @@ public function test_status_attribute_recently_fetched(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'is_active' => true, 'is_active' => true,
'last_fetched_at' => now()->subHour() 'last_fetched_at' => now()->subHour(),
]); ]);
$this->assertEquals('Recently fetched', $feed->status); $this->assertEquals('Recently fetched', $feed->status);
@ -95,7 +95,7 @@ public function test_status_attribute_fetched_hours_ago(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'is_active' => true, 'is_active' => true,
'last_fetched_at' => now()->subHours(5)->startOfHour() 'last_fetched_at' => now()->subHours(5)->startOfHour(),
]); ]);
$this->assertStringContainsString('Fetched', $feed->status); $this->assertStringContainsString('Fetched', $feed->status);
@ -106,7 +106,7 @@ public function test_status_attribute_fetched_days_ago(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'is_active' => true, 'is_active' => true,
'last_fetched_at' => now()->subDays(3) 'last_fetched_at' => now()->subDays(3),
]); ]);
$this->assertStringStartsWith('Fetched', $feed->status); $this->assertStringStartsWith('Fetched', $feed->status);
@ -126,10 +126,10 @@ public function test_belongs_to_language_relationship(): void
public function test_has_many_articles_relationship(): void public function test_has_many_articles_relationship(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$article1 = Article::factory()->create(['feed_id' => $feed->id]); $article1 = Article::factory()->create(['feed_id' => $feed->id]);
$article2 = Article::factory()->create(['feed_id' => $feed->id]); $article2 = Article::factory()->create(['feed_id' => $feed->id]);
// Create article for different feed // Create article for different feed
$otherFeed = Feed::factory()->create(); $otherFeed = Feed::factory()->create();
Article::factory()->create(['feed_id' => $otherFeed->id]); Article::factory()->create(['feed_id' => $otherFeed->id]);
@ -153,14 +153,14 @@ public function test_belongs_to_many_channels_relationship(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
Route::create([ Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'is_active' => false, 'is_active' => false,
'priority' => 50 'priority' => 50,
]); ]);
$channels = $feed->channels; $channels = $feed->channels;
@ -171,8 +171,8 @@ public function test_belongs_to_many_channels_relationship(): void
// Test pivot data // Test pivot data
$channel1FromRelation = $channels->find($channel1->id); $channel1FromRelation = $channels->find($channel1->id);
$this->assertEquals(1, $channel1FromRelation->pivot->is_active); $this->assertEquals(1, $channel1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $channel1FromRelation->pivot->priority); $this->assertEquals(100, $channel1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
} }
public function test_active_channels_relationship(): void public function test_active_channels_relationship(): void
@ -187,21 +187,21 @@ public function test_active_channels_relationship(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $activeChannel1->id, 'platform_channel_id' => $activeChannel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
Route::create([ Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $activeChannel2->id, 'platform_channel_id' => $activeChannel2->id,
'is_active' => true, 'is_active' => true,
'priority' => 200 'priority' => 200,
]); ]);
Route::create([ Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $inactiveChannel->id, 'platform_channel_id' => $inactiveChannel->id,
'is_active' => false, 'is_active' => false,
'priority' => 150 'priority' => 150,
]); ]);
$activeChannels = $feed->activeChannels; $activeChannels = $feed->activeChannels;
@ -226,7 +226,7 @@ public function test_feed_creation_with_factory(): void
$this->assertIsString($feed->url); $this->assertIsString($feed->url);
$this->assertIsString($feed->type); $this->assertIsString($feed->type);
// Language ID may be null as it's nullable in the database // Language ID may be null as it's nullable in the database
$this->assertTrue($feed->language_id === null || is_int($feed->language_id)); $this->assertTrue($feed->language_id === null || is_int($feed->language_id)); // @phpstan-ignore booleanOr.rightAlwaysTrue
$this->assertIsBool($feed->is_active); $this->assertIsBool($feed->is_active);
$this->assertIsArray($feed->settings); $this->assertIsArray($feed->settings);
} }
@ -244,7 +244,7 @@ public function test_feed_creation_with_explicit_values(): void
'language_id' => $language->id, 'language_id' => $language->id,
'description' => 'Test description', 'description' => 'Test description',
'settings' => $settings, 'settings' => $settings,
'is_active' => false 'is_active' => false,
]); ]);
$this->assertEquals('Test Feed', $feed->name); $this->assertEquals('Test Feed', $feed->name);
@ -260,12 +260,12 @@ public function test_feed_update(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'name' => 'Original Name', 'name' => 'Original Name',
'is_active' => true 'is_active' => true,
]); ]);
$feed->update([ $feed->update([
'name' => 'Updated Name', 'name' => 'Updated Name',
'is_active' => false 'is_active' => false,
]); ]);
$feed->refresh(); $feed->refresh();
@ -298,13 +298,13 @@ public function test_feed_settings_can_be_complex_structure(): void
'parsing' => [ 'parsing' => [
'selector' => 'article.post', 'selector' => 'article.post',
'title_selector' => 'h1', 'title_selector' => 'h1',
'content_selector' => '.content' 'content_selector' => '.content',
], ],
'filters' => ['min_length' => 100], 'filters' => ['min_length' => 100],
'schedule' => [ 'schedule' => [
'enabled' => true, 'enabled' => true,
'interval' => 3600 'interval' => 3600,
] ],
]; ];
$feed = Feed::factory()->create(['settings' => $complexSettings]); $feed = Feed::factory()->create(['settings' => $complexSettings]);
@ -330,4 +330,4 @@ public function test_feed_timestamps(): void
$this->assertInstanceOf(\Carbon\Carbon::class, $feed->created_at); $this->assertInstanceOf(\Carbon\Carbon::class, $feed->created_at);
$this->assertInstanceOf(\Carbon\Carbon::class, $feed->updated_at); $this->assertInstanceOf(\Carbon\Carbon::class, $feed->updated_at);
} }
} }

View file

@ -15,8 +15,8 @@ class KeywordTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['feed_id', 'platform_channel_id', 'keyword', 'is_active']; $fillableFields = ['feed_id', 'platform_channel_id', 'keyword', 'is_active'];
$keyword = new Keyword(); $keyword = new Keyword;
$this->assertEquals($fillableFields, $keyword->getFillable()); $this->assertEquals($fillableFields, $keyword->getFillable());
} }
@ -24,12 +24,12 @@ public function test_casts_is_active_to_boolean(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$keyword = Keyword::create([ $keyword = Keyword::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test', 'keyword' => 'test',
'is_active' => '1' 'is_active' => '1',
]); ]);
$this->assertIsBool($keyword->is_active); $this->assertIsBool($keyword->is_active);
@ -37,7 +37,7 @@ public function test_casts_is_active_to_boolean(): void
$keyword->update(['is_active' => '0']); $keyword->update(['is_active' => '0']);
$keyword->refresh(); $keyword->refresh();
$this->assertIsBool($keyword->is_active); $this->assertIsBool($keyword->is_active);
$this->assertFalse($keyword->is_active); $this->assertFalse($keyword->is_active);
} }
@ -46,12 +46,12 @@ public function test_belongs_to_feed_relationship(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$keyword = Keyword::create([ $keyword = Keyword::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertInstanceOf(Feed::class, $keyword->feed); $this->assertInstanceOf(Feed::class, $keyword->feed);
@ -63,12 +63,12 @@ public function test_belongs_to_platform_channel_relationship(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$keyword = Keyword::create([ $keyword = Keyword::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertInstanceOf(PlatformChannel::class, $keyword->platformChannel); $this->assertInstanceOf(PlatformChannel::class, $keyword->platformChannel);
@ -96,7 +96,7 @@ public function test_keyword_creation_with_explicit_values(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'Belgium', 'keyword' => 'Belgium',
'is_active' => false 'is_active' => false,
]); ]);
$this->assertEquals($feed->id, $keyword->feed_id); $this->assertEquals($feed->id, $keyword->feed_id);
@ -107,14 +107,15 @@ public function test_keyword_creation_with_explicit_values(): void
public function test_keyword_update(): void public function test_keyword_update(): void
{ {
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([ $keyword = Keyword::factory()->create([
'keyword' => 'original', 'keyword' => 'original',
'is_active' => true 'is_active' => true,
]); ]);
$keyword->update([ $keyword->update([
'keyword' => 'updated', 'keyword' => 'updated',
'is_active' => false 'is_active' => false,
]); ]);
$keyword->refresh(); $keyword->refresh();
@ -125,6 +126,7 @@ public function test_keyword_update(): void
public function test_keyword_deletion(): void public function test_keyword_deletion(): void
{ {
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create(); $keyword = Keyword::factory()->create();
$keywordId = $keyword->id; $keywordId = $keyword->id;
@ -145,7 +147,7 @@ public function test_keyword_with_special_characters(): void
'keyword with spaces', 'keyword with spaces',
'UPPERCASE', 'UPPERCASE',
'lowercase', 'lowercase',
'MixedCase' 'MixedCase',
]; ];
foreach ($specialKeywords as $keywordText) { foreach ($specialKeywords as $keywordText) {
@ -153,14 +155,14 @@ public function test_keyword_with_special_characters(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => $keywordText, 'keyword' => $keywordText,
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($keywordText, $keyword->keyword); $this->assertEquals($keywordText, $keyword->keyword);
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'keyword' => $keywordText, 'keyword' => $keywordText,
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]); ]);
} }
} }
@ -174,26 +176,26 @@ public function test_multiple_keywords_for_same_route(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'keyword1', 'keyword' => 'keyword1',
'is_active' => true 'is_active' => true,
]); ]);
$keyword2 = Keyword::create([ $keyword2 = Keyword::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'keyword2', 'keyword' => 'keyword2',
'is_active' => false 'is_active' => false,
]); ]);
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'id' => $keyword1->id, 'id' => $keyword1->id,
'keyword' => 'keyword1', 'keyword' => 'keyword1',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'id' => $keyword2->id, 'id' => $keyword2->id,
'keyword' => 'keyword2', 'keyword' => 'keyword2',
'is_active' => false 'is_active' => false,
]); ]);
} }
@ -207,17 +209,17 @@ public function test_keyword_uniqueness_constraint(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'unique_keyword', 'keyword' => 'unique_keyword',
'is_active' => true 'is_active' => true,
]); ]);
// Attempt to create duplicate should fail // Attempt to create duplicate should fail
$this->expectException(\Illuminate\Database\QueryException::class); $this->expectException(\Illuminate\Database\QueryException::class);
Keyword::create([ Keyword::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'unique_keyword', 'keyword' => 'unique_keyword',
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -233,14 +235,14 @@ public function test_same_keyword_different_routes_allowed(): void
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'keyword' => 'common_keyword', 'keyword' => 'common_keyword',
'is_active' => true 'is_active' => true,
]); ]);
$keyword2 = Keyword::create([ $keyword2 = Keyword::create([
'feed_id' => $feed2->id, 'feed_id' => $feed2->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'keyword' => 'common_keyword', 'keyword' => 'common_keyword',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('keywords', ['id' => $keyword1->id]); $this->assertDatabaseHas('keywords', ['id' => $keyword1->id]);
@ -250,6 +252,7 @@ public function test_same_keyword_different_routes_allowed(): void
public function test_keyword_timestamps(): void public function test_keyword_timestamps(): void
{ {
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create(); $keyword = Keyword::factory()->create();
$this->assertNotNull($keyword->created_at); $this->assertNotNull($keyword->created_at);
@ -267,7 +270,7 @@ public function test_keyword_default_active_state(): void
$keyword = Keyword::create([ $keyword = Keyword::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test' 'keyword' => 'test',
]); ]);
// Refresh to get the actual database values including defaults // Refresh to get the actual database values including defaults
@ -277,4 +280,4 @@ public function test_keyword_default_active_state(): void
$this->assertIsBool($keyword->is_active); $this->assertIsBool($keyword->is_active);
$this->assertTrue($keyword->is_active); $this->assertTrue($keyword->is_active);
} }
} }

View file

@ -16,15 +16,15 @@ class LanguageTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['short_code', 'name', 'native_name', 'is_active']; $fillableFields = ['short_code', 'name', 'native_name', 'is_active'];
$language = new Language(); $language = new Language;
$this->assertEquals($fillableFields, $language->getFillable()); $this->assertEquals($fillableFields, $language->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$language = new Language(); $language = new Language;
$this->assertEquals('languages', $language->getTable()); $this->assertEquals('languages', $language->getTable());
} }
@ -37,7 +37,7 @@ public function test_casts_is_active_to_boolean(): void
$language->update(['is_active' => '0']); $language->update(['is_active' => '0']);
$language->refresh(); $language->refresh();
$this->assertIsBool($language->is_active); $this->assertIsBool($language->is_active);
$this->assertFalse($language->is_active); $this->assertFalse($language->is_active);
} }
@ -51,7 +51,7 @@ public function test_belongs_to_many_platform_instances_relationship(): void
// Attach with required platform_language_id // Attach with required platform_language_id
$language->platformInstances()->attach([ $language->platformInstances()->attach([
$instance1->id => ['platform_language_id' => 1], $instance1->id => ['platform_language_id' => 1],
$instance2->id => ['platform_language_id' => 2] $instance2->id => ['platform_language_id' => 2],
]); ]);
$instances = $language->platformInstances; $instances = $language->platformInstances;
@ -64,10 +64,10 @@ public function test_belongs_to_many_platform_instances_relationship(): void
public function test_has_many_platform_channels_relationship(): void public function test_has_many_platform_channels_relationship(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$channel1 = PlatformChannel::factory()->create(['language_id' => $language->id]); $channel1 = PlatformChannel::factory()->create(['language_id' => $language->id]);
$channel2 = PlatformChannel::factory()->create(['language_id' => $language->id]); $channel2 = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Create channel for different language // Create channel for different language
$otherLanguage = Language::factory()->create(); $otherLanguage = Language::factory()->create();
PlatformChannel::factory()->create(['language_id' => $otherLanguage->id]); PlatformChannel::factory()->create(['language_id' => $otherLanguage->id]);
@ -83,10 +83,10 @@ public function test_has_many_platform_channels_relationship(): void
public function test_has_many_feeds_relationship(): void public function test_has_many_feeds_relationship(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
$feed1 = Feed::factory()->create(['language_id' => $language->id]); $feed1 = Feed::factory()->create(['language_id' => $language->id]);
$feed2 = Feed::factory()->create(['language_id' => $language->id]); $feed2 = Feed::factory()->create(['language_id' => $language->id]);
// Create feed for different language // Create feed for different language
$otherLanguage = Language::factory()->create(); $otherLanguage = Language::factory()->create();
Feed::factory()->create(['language_id' => $otherLanguage->id]); Feed::factory()->create(['language_id' => $otherLanguage->id]);
@ -115,7 +115,7 @@ public function test_language_creation_with_explicit_values(): void
'short_code' => 'fr', 'short_code' => 'fr',
'name' => 'French', 'name' => 'French',
'native_name' => 'Français', 'native_name' => 'Français',
'is_active' => false 'is_active' => false,
]); ]);
$this->assertEquals('fr', $language->short_code); $this->assertEquals('fr', $language->short_code);
@ -139,12 +139,12 @@ public function test_language_update(): void
{ {
$language = Language::factory()->create([ $language = Language::factory()->create([
'name' => 'Original Name', 'name' => 'Original Name',
'is_active' => true 'is_active' => true,
]); ]);
$language->update([ $language->update([
'name' => 'Updated Name', 'name' => 'Updated Name',
'is_active' => false 'is_active' => false,
]); ]);
$language->refresh(); $language->refresh();
@ -180,7 +180,7 @@ public function test_language_can_have_empty_native_name(): void
public function test_language_short_code_variations(): void public function test_language_short_code_variations(): void
{ {
$shortCodes = ['en', 'fr', 'es', 'de', 'zh', 'pt', 'nl', 'it']; $shortCodes = ['en', 'fr', 'es', 'de', 'zh', 'pt', 'nl', 'it'];
foreach ($shortCodes as $code) { foreach ($shortCodes as $code) {
$language = Language::factory()->create(['short_code' => $code]); $language = Language::factory()->create(['short_code' => $code]);
$this->assertEquals($code, $language->short_code); $this->assertEquals($code, $language->short_code);
@ -208,7 +208,7 @@ public function test_language_can_have_multiple_platform_instances(): void
$language->platformInstances()->attach([ $language->platformInstances()->attach([
$instance1->id => ['platform_language_id' => 1], $instance1->id => ['platform_language_id' => 1],
$instance2->id => ['platform_language_id' => 2], $instance2->id => ['platform_language_id' => 2],
$instance3->id => ['platform_language_id' => 3] $instance3->id => ['platform_language_id' => 3],
]); ]);
$instances = $language->platformInstances; $instances = $language->platformInstances;
@ -245,13 +245,13 @@ public function test_multiple_languages_with_same_name_different_regions(): void
$englishUS = Language::factory()->create([ $englishUS = Language::factory()->create([
'short_code' => 'en-US', 'short_code' => 'en-US',
'name' => 'English (United States)', 'name' => 'English (United States)',
'native_name' => 'English' 'native_name' => 'English',
]); ]);
$englishGB = Language::factory()->create([ $englishGB = Language::factory()->create([
'short_code' => 'en-GB', 'short_code' => 'en-GB',
'name' => 'English (United Kingdom)', 'name' => 'English (United Kingdom)',
'native_name' => 'English' 'native_name' => 'English',
]); ]);
$this->assertEquals('English', $englishUS->native_name); $this->assertEquals('English', $englishUS->native_name);
@ -272,7 +272,7 @@ public function test_language_with_complex_native_name(): void
foreach ($complexLanguages as $langData) { foreach ($complexLanguages as $langData) {
$language = Language::factory()->create($langData); $language = Language::factory()->create($langData);
$this->assertEquals($langData['short_code'], $language->short_code); $this->assertEquals($langData['short_code'], $language->short_code);
$this->assertEquals($langData['name'], $language->name); $this->assertEquals($langData['name'], $language->name);
$this->assertEquals($langData['native_name'], $language->native_name); $this->assertEquals($langData['native_name'], $language->native_name);
@ -291,23 +291,23 @@ public function test_language_active_and_inactive_states(): void
public function test_language_relationships_maintain_referential_integrity(): void public function test_language_relationships_maintain_referential_integrity(): void
{ {
$language = Language::factory()->create(); $language = Language::factory()->create();
// Create related models // Create related models
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
$feed = Feed::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]);
// Attach instance // Attach instance
$language->platformInstances()->attach($instance->id, [ $language->platformInstances()->attach($instance->id, [
'platform_language_id' => 1, 'platform_language_id' => 1,
'is_default' => true 'is_default' => true,
]); ]);
// Verify all relationships work // Verify all relationships work
$this->assertCount(1, $language->platformInstances); $this->assertCount(1, $language->platformInstances);
$this->assertCount(1, $language->platformChannels); $this->assertCount(1, $language->platformChannels);
$this->assertCount(1, $language->feeds); $this->assertCount(1, $language->feeds);
$this->assertEquals($language->id, $channel->language_id); $this->assertEquals($language->id, $channel->language_id);
$this->assertEquals($language->id, $feed->language_id); $this->assertEquals($language->id, $feed->language_id);
} }
@ -317,8 +317,8 @@ public function test_language_factory_unique_constraints(): void
// The factory should generate unique short codes // The factory should generate unique short codes
$language1 = Language::factory()->create(); $language1 = Language::factory()->create();
$language2 = Language::factory()->create(); $language2 = Language::factory()->create();
$this->assertNotEquals($language1->short_code, $language2->short_code); $this->assertNotEquals($language1->short_code, $language2->short_code);
$this->assertNotEquals($language1->name, $language2->name); $this->assertNotEquals($language1->name, $language2->name);
} }
} }

View file

@ -6,7 +6,6 @@
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase; use Tests\TestCase;
class PlatformAccountTest extends TestCase class PlatformAccountTest extends TestCase
@ -16,15 +15,15 @@ class PlatformAccountTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['platform', 'instance_url', 'username', 'password', 'settings', 'is_active', 'last_tested_at', 'status']; $fillableFields = ['platform', 'instance_url', 'username', 'password', 'settings', 'is_active', 'last_tested_at', 'status'];
$account = new PlatformAccount(); $account = new PlatformAccount;
$this->assertEquals($fillableFields, $account->getFillable()); $this->assertEquals($fillableFields, $account->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$account = new PlatformAccount(); $account = new PlatformAccount;
$this->assertEquals('platform_accounts', $account->getTable()); $this->assertEquals('platform_accounts', $account->getTable());
} }
@ -40,7 +39,7 @@ public function test_casts_platform_to_enum(): void
public function test_casts_settings_to_array(): void public function test_casts_settings_to_array(): void
{ {
$settings = ['key1' => 'value1', 'nested' => ['key2' => 'value2']]; $settings = ['key1' => 'value1', 'nested' => ['key2' => 'value2']];
$account = PlatformAccount::factory()->create(['settings' => $settings]); $account = PlatformAccount::factory()->create(['settings' => $settings]);
$this->assertIsArray($account->settings); $this->assertIsArray($account->settings);
@ -56,7 +55,7 @@ public function test_casts_is_active_to_boolean(): void
$account->update(['is_active' => '0']); $account->update(['is_active' => '0']);
$account->refresh(); $account->refresh();
$this->assertIsBool($account->is_active); $this->assertIsBool($account->is_active);
$this->assertFalse($account->is_active); $this->assertFalse($account->is_active);
} }
@ -73,12 +72,12 @@ public function test_casts_last_tested_at_to_datetime(): void
public function test_password_encryption_and_decryption(): void public function test_password_encryption_and_decryption(): void
{ {
$plainPassword = 'my-secret-password'; $plainPassword = 'my-secret-password';
$account = PlatformAccount::factory()->create(['password' => $plainPassword]); $account = PlatformAccount::factory()->create(['password' => $plainPassword]);
// Password should be decrypted when accessing // Password should be decrypted when accessing
$this->assertEquals($plainPassword, $account->password); $this->assertEquals($plainPassword, $account->password);
// But encrypted in the database // But encrypted in the database
$this->assertNotEquals($plainPassword, $account->getAttributes()['password']); $this->assertNotEquals($plainPassword, $account->getAttributes()['password']);
$this->assertNotNull($account->getAttributes()['password']); $this->assertNotNull($account->getAttributes()['password']);
@ -104,12 +103,11 @@ public function test_password_encryption_is_different_each_time(): void
$this->assertNotEquals($account1->getAttributes()['password'], $account2->getAttributes()['password']); $this->assertNotEquals($account1->getAttributes()['password'], $account2->getAttributes()['password']);
} }
public function test_password_decryption_handles_corruption(): void public function test_password_decryption_handles_corruption(): void
{ {
$account = PlatformAccount::factory()->create(); $account = PlatformAccount::factory()->create();
$originalPassword = $account->password; $originalPassword = $account->password;
// Since the password attribute has special handling, this test verifies the basic functionality // Since the password attribute has special handling, this test verifies the basic functionality
$this->assertNotNull($originalPassword); $this->assertNotNull($originalPassword);
$this->assertIsString($originalPassword); $this->assertIsString($originalPassword);
@ -120,17 +118,17 @@ public function test_get_active_static_method(): void
// Create active and inactive accounts // Create active and inactive accounts
$activeAccount1 = PlatformAccount::factory()->create([ $activeAccount1 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$activeAccount2 = PlatformAccount::factory()->create([ $activeAccount2 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$inactiveAccount = PlatformAccount::factory()->create([ $inactiveAccount = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => false 'is_active' => false,
]); ]);
$activeAccounts = PlatformAccount::getActive(PlatformEnum::LEMMY); $activeAccounts = PlatformAccount::getActive(PlatformEnum::LEMMY);
@ -146,17 +144,17 @@ public function test_set_as_active_method(): void
// Create multiple accounts for same platform // Create multiple accounts for same platform
$account1 = PlatformAccount::factory()->create([ $account1 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$account2 = PlatformAccount::factory()->create([ $account2 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$account3 = PlatformAccount::factory()->create([ $account3 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => false 'is_active' => false,
]); ]);
// Set account3 as active // Set account3 as active
@ -182,12 +180,12 @@ public function test_belongs_to_many_channels_relationship(): void
// Attach channels with pivot data // Attach channels with pivot data
$account->channels()->attach($channel1->id, [ $account->channels()->attach($channel1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$account->channels()->attach($channel2->id, [ $account->channels()->attach($channel2->id, [
'is_active' => false, 'is_active' => false,
'priority' => 50 'priority' => 50,
]); ]);
$channels = $account->channels; $channels = $account->channels;
@ -198,12 +196,12 @@ public function test_belongs_to_many_channels_relationship(): void
// Test pivot data // Test pivot data
$channel1FromRelation = $channels->find($channel1->id); $channel1FromRelation = $channels->find($channel1->id);
$this->assertEquals(1, $channel1FromRelation->pivot->is_active); $this->assertEquals(1, $channel1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $channel1FromRelation->pivot->priority); $this->assertEquals(100, $channel1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
$channel2FromRelation = $channels->find($channel2->id); $channel2FromRelation = $channels->find($channel2->id);
$this->assertEquals(0, $channel2FromRelation->pivot->is_active); $this->assertEquals(0, $channel2FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(50, $channel2FromRelation->pivot->priority); $this->assertEquals(50, $channel2FromRelation->pivot->priority); // @phpstan-ignore property.notFound
} }
public function test_active_channels_relationship(): void public function test_active_channels_relationship(): void
@ -216,17 +214,17 @@ public function test_active_channels_relationship(): void
// Attach channels // Attach channels
$account->channels()->attach($activeChannel1->id, [ $account->channels()->attach($activeChannel1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$account->channels()->attach($activeChannel2->id, [ $account->channels()->attach($activeChannel2->id, [
'is_active' => true, 'is_active' => true,
'priority' => 200 'priority' => 200,
]); ]);
$account->channels()->attach($inactiveChannel->id, [ $account->channels()->attach($inactiveChannel->id, [
'is_active' => false, 'is_active' => false,
'priority' => 150 'priority' => 150,
]); ]);
$activeChannels = $account->activeChannels; $activeChannels = $account->activeChannels;
@ -271,7 +269,7 @@ public function test_account_creation_with_explicit_values(): void
'settings' => $settings, 'settings' => $settings,
'is_active' => false, 'is_active' => false,
'last_tested_at' => $timestamp, 'last_tested_at' => $timestamp,
'status' => 'working' 'status' => 'working',
]); ]);
$this->assertEquals(PlatformEnum::LEMMY, $account->platform); $this->assertEquals(PlatformEnum::LEMMY, $account->platform);
@ -302,12 +300,12 @@ public function test_account_update(): void
{ {
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'username' => 'original_user', 'username' => 'original_user',
'is_active' => true 'is_active' => true,
]); ]);
$account->update([ $account->update([
'username' => 'updated_user', 'username' => 'updated_user',
'is_active' => false 'is_active' => false,
]); ]);
$account->refresh(); $account->refresh();
@ -339,13 +337,13 @@ public function test_account_settings_can_be_complex_structure(): void
$complexSettings = [ $complexSettings = [
'authentication' => [ 'authentication' => [
'method' => 'jwt', 'method' => 'jwt',
'timeout' => 30 'timeout' => 30,
], ],
'features' => ['posting', 'commenting'], 'features' => ['posting', 'commenting'],
'rate_limits' => [ 'rate_limits' => [
'posts_per_hour' => 10, 'posts_per_hour' => 10,
'comments_per_hour' => 50 'comments_per_hour' => 50,
] ],
]; ];
$account = PlatformAccount::factory()->create(['settings' => $complexSettings]); $account = PlatformAccount::factory()->create(['settings' => $complexSettings]);
@ -383,7 +381,7 @@ public function test_account_can_have_multiple_channels_with_different_prioritie
$account->channels()->attach([ $account->channels()->attach([
$channel1->id => ['is_active' => true, 'priority' => 300], $channel1->id => ['is_active' => true, 'priority' => 300],
$channel2->id => ['is_active' => true, 'priority' => 100], $channel2->id => ['is_active' => true, 'priority' => 100],
$channel3->id => ['is_active' => false, 'priority' => 200] $channel3->id => ['is_active' => false, 'priority' => 200],
]); ]);
$allChannels = $account->channels; $allChannels = $account->channels;
@ -394,24 +392,24 @@ public function test_account_can_have_multiple_channels_with_different_prioritie
// Test that we can access pivot data // Test that we can access pivot data
foreach ($allChannels as $channel) { foreach ($allChannels as $channel) {
$this->assertNotNull($channel->pivot->priority); $this->assertNotNull($channel->pivot->priority); // @phpstan-ignore property.notFound
$this->assertIsInt($channel->pivot->is_active); $this->assertIsInt($channel->pivot->is_active); // @phpstan-ignore property.notFound
} }
} }
public function test_password_withoutObjectCaching_prevents_caching(): void public function test_password_without_object_caching_prevents_caching(): void
{ {
$account = PlatformAccount::factory()->create(['password' => 'original']); $account = PlatformAccount::factory()->create(['password' => 'original']);
// Access password to potentially cache it // Access password to potentially cache it
$originalPassword = $account->password; $originalPassword = $account->password;
$this->assertEquals('original', $originalPassword); $this->assertEquals('original', $originalPassword);
// Update password directly in database // Update password directly in database
$account->update(['password' => 'updated']); $account->update(['password' => 'updated']);
// Since withoutObjectCaching is used, the new value should be retrieved // Since withoutObjectCaching is used, the new value should be retrieved
$account->refresh(); $account->refresh();
$this->assertEquals('updated', $account->password); $this->assertEquals('updated', $account->password);
} }
} }

View file

@ -18,15 +18,15 @@ class PlatformChannelTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['platform_instance_id', 'name', 'display_name', 'channel_id', 'description', 'language_id', 'is_active']; $fillableFields = ['platform_instance_id', 'name', 'display_name', 'channel_id', 'description', 'language_id', 'is_active'];
$channel = new PlatformChannel(); $channel = new PlatformChannel;
$this->assertEquals($fillableFields, $channel->getFillable()); $this->assertEquals($fillableFields, $channel->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$channel = new PlatformChannel(); $channel = new PlatformChannel;
$this->assertEquals('platform_channels', $channel->getTable()); $this->assertEquals('platform_channels', $channel->getTable());
} }
@ -39,7 +39,7 @@ public function test_casts_is_active_to_boolean(): void
$channel->update(['is_active' => '0']); $channel->update(['is_active' => '0']);
$channel->refresh(); $channel->refresh();
$this->assertIsBool($channel->is_active); $this->assertIsBool($channel->is_active);
$this->assertFalse($channel->is_active); $this->assertFalse($channel->is_active);
} }
@ -73,12 +73,12 @@ public function test_belongs_to_many_platform_accounts_relationship(): void
// Attach accounts with pivot data // Attach accounts with pivot data
$channel->platformAccounts()->attach($account1->id, [ $channel->platformAccounts()->attach($account1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$channel->platformAccounts()->attach($account2->id, [ $channel->platformAccounts()->attach($account2->id, [
'is_active' => false, 'is_active' => false,
'priority' => 50 'priority' => 50,
]); ]);
$accounts = $channel->platformAccounts; $accounts = $channel->platformAccounts;
@ -89,12 +89,12 @@ public function test_belongs_to_many_platform_accounts_relationship(): void
// Test pivot data // Test pivot data
$account1FromRelation = $accounts->find($account1->id); $account1FromRelation = $accounts->find($account1->id);
$this->assertEquals(1, $account1FromRelation->pivot->is_active); $this->assertEquals(1, $account1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $account1FromRelation->pivot->priority); $this->assertEquals(100, $account1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
$account2FromRelation = $accounts->find($account2->id); $account2FromRelation = $accounts->find($account2->id);
$this->assertEquals(0, $account2FromRelation->pivot->is_active); $this->assertEquals(0, $account2FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(50, $account2FromRelation->pivot->priority); $this->assertEquals(50, $account2FromRelation->pivot->priority); // @phpstan-ignore property.notFound
} }
public function test_active_platform_accounts_relationship(): void public function test_active_platform_accounts_relationship(): void
@ -107,17 +107,17 @@ public function test_active_platform_accounts_relationship(): void
// Attach accounts // Attach accounts
$channel->platformAccounts()->attach($activeAccount1->id, [ $channel->platformAccounts()->attach($activeAccount1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$channel->platformAccounts()->attach($activeAccount2->id, [ $channel->platformAccounts()->attach($activeAccount2->id, [
'is_active' => true, 'is_active' => true,
'priority' => 200 'priority' => 200,
]); ]);
$channel->platformAccounts()->attach($inactiveAccount->id, [ $channel->platformAccounts()->attach($inactiveAccount->id, [
'is_active' => false, 'is_active' => false,
'priority' => 150 'priority' => 150,
]); ]);
$activeAccounts = $channel->activePlatformAccounts; $activeAccounts = $channel->activePlatformAccounts;
@ -133,7 +133,7 @@ public function test_full_name_attribute(): void
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.example.com']); $instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.example.com']);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'name' => 'technology' 'name' => 'technology',
]); ]);
$this->assertEquals('https://lemmy.example.com/c/technology', $channel->full_name); $this->assertEquals('https://lemmy.example.com/c/technology', $channel->full_name);
@ -150,14 +150,14 @@ public function test_belongs_to_many_feeds_relationship(): void
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
Route::create([ Route::create([
'feed_id' => $feed2->id, 'feed_id' => $feed2->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => false, 'is_active' => false,
'priority' => 50 'priority' => 50,
]); ]);
$feeds = $channel->feeds; $feeds = $channel->feeds;
@ -168,8 +168,8 @@ public function test_belongs_to_many_feeds_relationship(): void
// Test pivot data // Test pivot data
$feed1FromRelation = $feeds->find($feed1->id); $feed1FromRelation = $feeds->find($feed1->id);
$this->assertEquals(1, $feed1FromRelation->pivot->is_active); $this->assertEquals(1, $feed1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $feed1FromRelation->pivot->priority); $this->assertEquals(100, $feed1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
} }
public function test_active_feeds_relationship(): void public function test_active_feeds_relationship(): void
@ -184,21 +184,21 @@ public function test_active_feeds_relationship(): void
'feed_id' => $activeFeed1->id, 'feed_id' => $activeFeed1->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
Route::create([ Route::create([
'feed_id' => $activeFeed2->id, 'feed_id' => $activeFeed2->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 200 'priority' => 200,
]); ]);
Route::create([ Route::create([
'feed_id' => $inactiveFeed->id, 'feed_id' => $inactiveFeed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => false, 'is_active' => false,
'priority' => 150 'priority' => 150,
]); ]);
$activeFeeds = $channel->activeFeeds; $activeFeeds = $channel->activeFeeds;
@ -221,7 +221,7 @@ public function test_channel_creation_with_factory(): void
$this->assertInstanceOf(PlatformChannel::class, $channel); $this->assertInstanceOf(PlatformChannel::class, $channel);
$this->assertNotNull($channel->platform_instance_id); $this->assertNotNull($channel->platform_instance_id);
$this->assertIsString($channel->name); $this->assertIsString($channel->name);
$this->assertIsString($channel->channel_id); $this->assertIsString($channel->channel_id); // @phpstan-ignore method.impossibleType
$this->assertIsBool($channel->is_active); $this->assertIsBool($channel->is_active);
} }
@ -237,7 +237,7 @@ public function test_channel_creation_with_explicit_values(): void
'channel_id' => 'channel_123', 'channel_id' => 'channel_123',
'description' => 'A test channel', 'description' => 'A test channel',
'language_id' => $language->id, 'language_id' => $language->id,
'is_active' => false 'is_active' => false,
]); ]);
$this->assertEquals($instance->id, $channel->platform_instance_id); $this->assertEquals($instance->id, $channel->platform_instance_id);
@ -253,12 +253,12 @@ public function test_channel_update(): void
{ {
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'original_name', 'name' => 'original_name',
'is_active' => true 'is_active' => true,
]); ]);
$channel->update([ $channel->update([
'name' => 'updated_name', 'name' => 'updated_name',
'is_active' => false 'is_active' => false,
]); ]);
$channel->refresh(); $channel->refresh();
@ -281,7 +281,7 @@ public function test_channel_with_display_name(): void
{ {
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'tech', 'name' => 'tech',
'display_name' => 'Technology Discussion' 'display_name' => 'Technology Discussion',
]); ]);
$this->assertEquals('tech', $channel->name); $this->assertEquals('tech', $channel->name);
@ -292,7 +292,7 @@ public function test_channel_without_display_name(): void
{ {
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'general', 'name' => 'general',
'display_name' => 'General' 'display_name' => 'General',
]); ]);
$this->assertEquals('general', $channel->name); $this->assertEquals('general', $channel->name);
@ -320,7 +320,7 @@ public function test_channel_can_have_multiple_accounts_with_different_prioritie
$channel->platformAccounts()->attach([ $channel->platformAccounts()->attach([
$account1->id => ['is_active' => true, 'priority' => 300], $account1->id => ['is_active' => true, 'priority' => 300],
$account2->id => ['is_active' => true, 'priority' => 100], $account2->id => ['is_active' => true, 'priority' => 100],
$account3->id => ['is_active' => false, 'priority' => 200] $account3->id => ['is_active' => false, 'priority' => 200],
]); ]);
$allAccounts = $channel->platformAccounts; $allAccounts = $channel->platformAccounts;
@ -331,8 +331,8 @@ public function test_channel_can_have_multiple_accounts_with_different_prioritie
// Test that we can access pivot data // Test that we can access pivot data
foreach ($allAccounts as $account) { foreach ($allAccounts as $account) {
$this->assertNotNull($account->pivot->priority); $this->assertNotNull($account->pivot->priority); // @phpstan-ignore property.notFound
$this->assertIsInt($account->pivot->is_active); $this->assertIsInt($account->pivot->is_active); // @phpstan-ignore property.notFound
} }
} }
} }

View file

@ -16,15 +16,15 @@ class PlatformInstanceTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['platform', 'url', 'name', 'description', 'is_active']; $fillableFields = ['platform', 'url', 'name', 'description', 'is_active'];
$instance = new PlatformInstance(); $instance = new PlatformInstance;
$this->assertEquals($fillableFields, $instance->getFillable()); $this->assertEquals($fillableFields, $instance->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$instance = new PlatformInstance(); $instance = new PlatformInstance;
$this->assertEquals('platform_instances', $instance->getTable()); $this->assertEquals('platform_instances', $instance->getTable());
} }
@ -46,7 +46,7 @@ public function test_casts_is_active_to_boolean(): void
$instance->update(['is_active' => '0']); $instance->update(['is_active' => '0']);
$instance->refresh(); $instance->refresh();
$this->assertIsBool($instance->is_active); $this->assertIsBool($instance->is_active);
$this->assertFalse($instance->is_active); $this->assertFalse($instance->is_active);
} }
@ -54,10 +54,10 @@ public function test_casts_is_active_to_boolean(): void
public function test_has_many_channels_relationship(): void public function test_has_many_channels_relationship(): void
{ {
$instance = PlatformInstance::factory()->create(); $instance = PlatformInstance::factory()->create();
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]); $channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]);
$channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]); $channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]);
// Create channel for different instance // Create channel for different instance
$otherInstance = PlatformInstance::factory()->create(); $otherInstance = PlatformInstance::factory()->create();
PlatformChannel::factory()->create(['platform_instance_id' => $otherInstance->id]); PlatformChannel::factory()->create(['platform_instance_id' => $otherInstance->id]);
@ -79,12 +79,12 @@ public function test_belongs_to_many_languages_relationship(): void
// Attach languages with pivot data // Attach languages with pivot data
$instance->languages()->attach($language1->id, [ $instance->languages()->attach($language1->id, [
'platform_language_id' => 1, 'platform_language_id' => 1,
'is_default' => true 'is_default' => true,
]); ]);
$instance->languages()->attach($language2->id, [ $instance->languages()->attach($language2->id, [
'platform_language_id' => 2, 'platform_language_id' => 2,
'is_default' => false 'is_default' => false,
]); ]);
$languages = $instance->languages; $languages = $instance->languages;
@ -95,27 +95,27 @@ public function test_belongs_to_many_languages_relationship(): void
// Test pivot data // Test pivot data
$language1FromRelation = $languages->find($language1->id); $language1FromRelation = $languages->find($language1->id);
$this->assertEquals(1, $language1FromRelation->pivot->platform_language_id); $this->assertEquals(1, $language1FromRelation->pivot->platform_language_id); // @phpstan-ignore property.notFound
$this->assertEquals(1, $language1FromRelation->pivot->is_default); // Database returns 1 for true $this->assertEquals(1, $language1FromRelation->pivot->is_default); // @phpstan-ignore property.notFound
$language2FromRelation = $languages->find($language2->id); $language2FromRelation = $languages->find($language2->id);
$this->assertEquals(2, $language2FromRelation->pivot->platform_language_id); $this->assertEquals(2, $language2FromRelation->pivot->platform_language_id); // @phpstan-ignore property.notFound
$this->assertEquals(0, $language2FromRelation->pivot->is_default); // Database returns 0 for false $this->assertEquals(0, $language2FromRelation->pivot->is_default); // @phpstan-ignore property.notFound
} }
public function test_find_by_url_static_method(): void public function test_find_by_url_static_method(): void
{ {
$url = 'https://lemmy.world'; $url = 'https://lemmy.world';
$instance1 = PlatformInstance::factory()->create([ $instance1 = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => $url 'url' => $url,
]); ]);
// Create instance with different URL // Create instance with different URL
PlatformInstance::factory()->create([ PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.ml' 'url' => 'https://lemmy.ml',
]); ]);
$foundInstance = PlatformInstance::findByUrl(PlatformEnum::LEMMY, $url); $foundInstance = PlatformInstance::findByUrl(PlatformEnum::LEMMY, $url);
@ -136,11 +136,11 @@ public function test_find_by_url_returns_null_when_not_found(): void
public function test_find_by_url_filters_by_platform(): void public function test_find_by_url_filters_by_platform(): void
{ {
$url = 'https://example.com'; $url = 'https://example.com';
// Create instance with same URL but different platform won't be found // Create instance with same URL but different platform won't be found
PlatformInstance::factory()->create([ PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => $url 'url' => $url,
]); ]);
// Since we only have LEMMY in the enum, this test demonstrates the filtering logic // Since we only have LEMMY in the enum, this test demonstrates the filtering logic
@ -166,7 +166,7 @@ public function test_instance_creation_with_explicit_values(): void
'url' => 'https://lemmy.world', 'url' => 'https://lemmy.world',
'name' => 'Lemmy World', 'name' => 'Lemmy World',
'description' => 'A general purpose Lemmy instance', 'description' => 'A general purpose Lemmy instance',
'is_active' => false 'is_active' => false,
]); ]);
$this->assertEquals(PlatformEnum::LEMMY, $instance->platform); $this->assertEquals(PlatformEnum::LEMMY, $instance->platform);
@ -191,12 +191,12 @@ public function test_instance_update(): void
{ {
$instance = PlatformInstance::factory()->create([ $instance = PlatformInstance::factory()->create([
'name' => 'Original Name', 'name' => 'Original Name',
'is_active' => true 'is_active' => true,
]); ]);
$instance->update([ $instance->update([
'name' => 'Updated Name', 'name' => 'Updated Name',
'is_active' => false 'is_active' => false,
]); ]);
$instance->refresh(); $instance->refresh();
@ -219,7 +219,7 @@ public function test_instance_can_have_null_description(): void
{ {
$instance = PlatformInstance::factory()->create(['description' => null]); $instance = PlatformInstance::factory()->create(['description' => null]);
$this->assertNull($instance->description); $this->assertNull($instance->description); // @phpstan-ignore method.impossibleType
} }
public function test_instance_can_have_empty_description(): void public function test_instance_can_have_empty_description(): void
@ -265,7 +265,7 @@ public function test_instance_can_have_multiple_languages(): void
$instance->languages()->attach([ $instance->languages()->attach([
$language1->id => ['platform_language_id' => 1, 'is_default' => true], $language1->id => ['platform_language_id' => 1, 'is_default' => true],
$language2->id => ['platform_language_id' => 2, 'is_default' => false], $language2->id => ['platform_language_id' => 2, 'is_default' => false],
$language3->id => ['platform_language_id' => 3, 'is_default' => false] $language3->id => ['platform_language_id' => 3, 'is_default' => false],
]); ]);
$languages = $instance->languages; $languages = $instance->languages;
@ -274,12 +274,12 @@ public function test_instance_can_have_multiple_languages(): void
// Test that we can access pivot data // Test that we can access pivot data
foreach ($languages as $language) { foreach ($languages as $language) {
$this->assertNotNull($language->pivot->platform_language_id); $this->assertNotNull($language->pivot->platform_language_id); // @phpstan-ignore property.notFound
$this->assertContains($language->pivot->is_default, [0, 1, true, false]); // Can be int or bool $this->assertContains($language->pivot->is_default, [0, 1, true, false]); // @phpstan-ignore property.notFound
} }
// Only one should be default // Only one should be default
$defaultLanguages = $languages->filter(fn($lang) => $lang->pivot->is_default); $defaultLanguages = $languages->filter(fn ($lang) => $lang->pivot->is_default); // @phpstan-ignore property.notFound
$this->assertCount(1, $defaultLanguages); $this->assertCount(1, $defaultLanguages);
} }
@ -301,12 +301,12 @@ public function test_multiple_instances_with_same_platform(): void
{ {
$instance1 = PlatformInstance::factory()->create([ $instance1 = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'name' => 'Lemmy World' 'name' => 'Lemmy World',
]); ]);
$instance2 = PlatformInstance::factory()->create([ $instance2 = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'name' => 'Lemmy ML' 'name' => 'Lemmy ML',
]); ]);
$this->assertEquals(PlatformEnum::LEMMY, $instance1->platform); $this->assertEquals(PlatformEnum::LEMMY, $instance1->platform);
@ -322,4 +322,4 @@ public function test_instance_platform_enum_string_value(): void
$this->assertEquals('lemmy', $instance->platform->value); $this->assertEquals('lemmy', $instance->platform->value);
$this->assertEquals(PlatformEnum::LEMMY, $instance->platform); $this->assertEquals(PlatformEnum::LEMMY, $instance->platform);
} }
} }

View file

@ -16,8 +16,8 @@ class RouteTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority']; $fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority'];
$route = new Route(); $route = new Route;
$this->assertEquals($fillableFields, $route->getFillable()); $this->assertEquals($fillableFields, $route->getFillable());
} }
@ -25,12 +25,12 @@ public function test_casts_is_active_to_boolean(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$route = Route::create([ $route = Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => '1', 'is_active' => '1',
'priority' => 50 'priority' => 50,
]); ]);
$this->assertIsBool($route->is_active); $this->assertIsBool($route->is_active);
@ -38,23 +38,23 @@ public function test_casts_is_active_to_boolean(): void
$route->update(['is_active' => '0']); $route->update(['is_active' => '0']);
$route->refresh(); $route->refresh();
$this->assertIsBool($route->is_active); $this->assertIsBool($route->is_active);
$this->assertFalse($route->is_active); $this->assertFalse($route->is_active);
} }
public function test_primary_key_configuration(): void public function test_primary_key_configuration(): void
{ {
$route = new Route(); $route = new Route;
$this->assertNull($route->getKeyName()); $this->assertNull($route->getKeyName()); // @phpstan-ignore method.impossibleType
$this->assertFalse($route->getIncrementing()); $this->assertFalse($route->getIncrementing());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$route = new Route(); $route = new Route;
$this->assertEquals('routes', $route->getTable()); $this->assertEquals('routes', $route->getTable());
} }
@ -62,12 +62,12 @@ public function test_belongs_to_feed_relationship(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$route = Route::create([ $route = Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$this->assertInstanceOf(Feed::class, $route->feed); $this->assertInstanceOf(Feed::class, $route->feed);
@ -79,12 +79,12 @@ public function test_belongs_to_platform_channel_relationship(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$route = Route::create([ $route = Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$this->assertInstanceOf(PlatformChannel::class, $route->platformChannel); $this->assertInstanceOf(PlatformChannel::class, $route->platformChannel);
@ -96,25 +96,27 @@ public function test_has_many_keywords_relationship(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$route = Route::create([ $route = Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Create keywords for this route // Create keywords for this route
/** @var Keyword $keyword1 */
$keyword1 = Keyword::factory()->create([ $keyword1 = Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test1' 'keyword' => 'test1',
]); ]);
/** @var Keyword $keyword2 */
$keyword2 = Keyword::factory()->create([ $keyword2 = Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test2' 'keyword' => 'test2',
]); ]);
// Create keyword for different route (should not be included) // Create keyword for different route (should not be included)
@ -122,7 +124,7 @@ public function test_has_many_keywords_relationship(): void
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $otherFeed->id, 'feed_id' => $otherFeed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'other' 'keyword' => 'other',
]); ]);
$keywords = $route->keywords; $keywords = $route->keywords;
@ -139,33 +141,34 @@ public function test_keywords_relationship_filters_by_feed_and_channel(): void
$feed2 = Feed::factory()->create(); $feed2 = Feed::factory()->create();
$channel1 = PlatformChannel::factory()->create(); $channel1 = PlatformChannel::factory()->create();
$channel2 = PlatformChannel::factory()->create(); $channel2 = PlatformChannel::factory()->create();
$route = Route::create([ $route = Route::create([
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Create keyword for this exact route // Create keyword for this exact route
/** @var Keyword $matchingKeyword */
$matchingKeyword = Keyword::factory()->create([ $matchingKeyword = Keyword::factory()->create([
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'keyword' => 'matching' 'keyword' => 'matching',
]); ]);
// Create keyword for same feed but different channel // Create keyword for same feed but different channel
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'keyword' => 'different_channel' 'keyword' => 'different_channel',
]); ]);
// Create keyword for same channel but different feed // Create keyword for same channel but different feed
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed2->id, 'feed_id' => $feed2->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'keyword' => 'different_feed' 'keyword' => 'different_feed',
]); ]);
$keywords = $route->keywords; $keywords = $route->keywords;
@ -195,7 +198,7 @@ public function test_route_creation_with_explicit_values(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => false, 'is_active' => false,
'priority' => 75 'priority' => 75,
]); ]);
$this->assertEquals($feed->id, $route->feed_id); $this->assertEquals($feed->id, $route->feed_id);
@ -206,14 +209,15 @@ public function test_route_creation_with_explicit_values(): void
public function test_route_update(): void public function test_route_update(): void
{ {
/** @var Route $route */
$route = Route::factory()->create([ $route = Route::factory()->create([
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$route->update([ $route->update([
'is_active' => false, 'is_active' => false,
'priority' => 25 'priority' => 25,
]); ]);
$route->refresh(); $route->refresh();
@ -226,26 +230,26 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void
{ {
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
$route = Route::create([ $route = Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'active_keyword', 'keyword' => 'active_keyword',
'is_active' => true 'is_active' => true,
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'inactive_keyword', 'keyword' => 'inactive_keyword',
'is_active' => false 'is_active' => false,
]); ]);
$keywords = $route->keywords; $keywords = $route->keywords;
@ -258,4 +262,4 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void
$this->assertEquals('active_keyword', $activeKeywords->first()->keyword); $this->assertEquals('active_keyword', $activeKeywords->first()->keyword);
$this->assertEquals('inactive_keyword', $inactiveKeywords->first()->keyword); $this->assertEquals('inactive_keyword', $inactiveKeywords->first()->keyword);
} }
} }

View file

@ -12,7 +12,7 @@ class LemmyRequestTest extends TestCase
public function test_constructor_with_simple_domain(): void public function test_constructor_with_simple_domain(): void
{ {
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
$this->assertNull($this->getPrivateProperty($request, 'token')); $this->assertNull($this->getPrivateProperty($request, 'token'));
@ -21,7 +21,7 @@ public function test_constructor_with_simple_domain(): void
public function test_constructor_with_https_url(): void public function test_constructor_with_https_url(): void
{ {
$request = new LemmyRequest('https://lemmy.world'); $request = new LemmyRequest('https://lemmy.world');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
} }
@ -29,7 +29,7 @@ public function test_constructor_with_https_url(): void
public function test_constructor_with_http_url(): void public function test_constructor_with_http_url(): void
{ {
$request = new LemmyRequest('http://lemmy.world'); $request = new LemmyRequest('http://lemmy.world');
$this->assertEquals('http', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('http', $this->getPrivateProperty($request, 'scheme'));
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
} }
@ -37,14 +37,14 @@ public function test_constructor_with_http_url(): void
public function test_constructor_with_trailing_slash(): void public function test_constructor_with_trailing_slash(): void
{ {
$request = new LemmyRequest('lemmy.world/'); $request = new LemmyRequest('lemmy.world/');
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
} }
public function test_constructor_with_full_url_and_trailing_slash(): void public function test_constructor_with_full_url_and_trailing_slash(): void
{ {
$request = new LemmyRequest('https://lemmy.world/'); $request = new LemmyRequest('https://lemmy.world/');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
} }
@ -52,14 +52,14 @@ public function test_constructor_with_full_url_and_trailing_slash(): void
public function test_constructor_with_token(): void public function test_constructor_with_token(): void
{ {
$request = new LemmyRequest('lemmy.world', 'test-token'); $request = new LemmyRequest('lemmy.world', 'test-token');
$this->assertEquals('test-token', $this->getPrivateProperty($request, 'token')); $this->assertEquals('test-token', $this->getPrivateProperty($request, 'token'));
} }
public function test_constructor_preserves_case_in_scheme_detection(): void public function test_constructor_preserves_case_in_scheme_detection(): void
{ {
$request = new LemmyRequest('HTTPS://lemmy.world'); $request = new LemmyRequest('HTTPS://lemmy.world');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
} }
@ -67,7 +67,7 @@ public function test_with_scheme_sets_https(): void
{ {
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$result = $request->withScheme('https'); $result = $request->withScheme('https');
$this->assertSame($request, $result); $this->assertSame($request, $result);
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
} }
@ -76,7 +76,7 @@ public function test_with_scheme_sets_http(): void
{ {
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$result = $request->withScheme('http'); $result = $request->withScheme('http');
$this->assertSame($request, $result); $this->assertSame($request, $result);
$this->assertEquals('http', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('http', $this->getPrivateProperty($request, 'scheme'));
} }
@ -85,7 +85,7 @@ public function test_with_scheme_normalizes_case(): void
{ {
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$request->withScheme('HTTPS'); $request->withScheme('HTTPS');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
} }
@ -93,9 +93,9 @@ public function test_with_scheme_ignores_invalid_schemes(): void
{ {
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$originalScheme = $this->getPrivateProperty($request, 'scheme'); $originalScheme = $this->getPrivateProperty($request, 'scheme');
$request->withScheme('ftp'); $request->withScheme('ftp');
$this->assertEquals($originalScheme, $this->getPrivateProperty($request, 'scheme')); $this->assertEquals($originalScheme, $this->getPrivateProperty($request, 'scheme'));
} }
@ -103,7 +103,7 @@ public function test_with_token_sets_token(): void
{ {
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$result = $request->withToken('new-token'); $result = $request->withToken('new-token');
$this->assertSame($request, $result); $this->assertSame($request, $result);
$this->assertEquals('new-token', $this->getPrivateProperty($request, 'token')); $this->assertEquals('new-token', $this->getPrivateProperty($request, 'token'));
} }
@ -111,27 +111,27 @@ public function test_with_token_sets_token(): void
public function test_get_without_token(): void public function test_get_without_token(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$response = $request->get('site'); $response = $request->get('site');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/site' return $httpRequest->url() === 'https://lemmy.world/api/v3/site'
&& !$httpRequest->hasHeader('Authorization'); && ! $httpRequest->hasHeader('Authorization');
}); });
} }
public function test_get_with_token(): void public function test_get_with_token(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world', 'test-token'); $request = new LemmyRequest('lemmy.world', 'test-token');
$response = $request->get('site'); $response = $request->get('site');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/site' return $httpRequest->url() === 'https://lemmy.world/api/v3/site'
&& $httpRequest->header('Authorization')[0] === 'Bearer test-token'; && $httpRequest->header('Authorization')[0] === 'Bearer test-token';
@ -141,15 +141,16 @@ public function test_get_with_token(): void
public function test_get_with_parameters(): void public function test_get_with_parameters(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$params = ['limit' => 10, 'page' => 1]; $params = ['limit' => 10, 'page' => 1];
$response = $request->get('posts', $params); $response = $request->get('posts', $params);
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) use ($params) { Http::assertSent(function ($httpRequest) {
$url = $httpRequest->url(); $url = $httpRequest->url();
return str_contains($url, 'https://lemmy.world/api/v3/posts') return str_contains($url, 'https://lemmy.world/api/v3/posts')
&& str_contains($url, 'limit=10') && str_contains($url, 'limit=10')
&& str_contains($url, 'page=1'); && str_contains($url, 'page=1');
@ -159,13 +160,13 @@ public function test_get_with_parameters(): void
public function test_get_with_http_scheme(): void public function test_get_with_http_scheme(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$request->withScheme('http'); $request->withScheme('http');
$response = $request->get('site'); $response = $request->get('site');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'http://lemmy.world/api/v3/site'; return $httpRequest->url() === 'http://lemmy.world/api/v3/site';
}); });
@ -174,28 +175,28 @@ public function test_get_with_http_scheme(): void
public function test_post_without_token(): void public function test_post_without_token(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$response = $request->post('login'); $response = $request->post('login');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/login' return $httpRequest->url() === 'https://lemmy.world/api/v3/login'
&& $httpRequest->method() === 'POST' && $httpRequest->method() === 'POST'
&& !$httpRequest->hasHeader('Authorization'); && ! $httpRequest->hasHeader('Authorization');
}); });
} }
public function test_post_with_token(): void public function test_post_with_token(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world', 'test-token'); $request = new LemmyRequest('lemmy.world', 'test-token');
$response = $request->post('login'); $response = $request->post('login');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/login' return $httpRequest->url() === 'https://lemmy.world/api/v3/login'
&& $httpRequest->method() === 'POST' && $httpRequest->method() === 'POST'
@ -206,13 +207,13 @@ public function test_post_with_token(): void
public function test_post_with_data(): void public function test_post_with_data(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$data = ['username' => 'test', 'password' => 'pass']; $data = ['username' => 'test', 'password' => 'pass'];
$response = $request->post('login', $data); $response = $request->post('login', $data);
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) use ($data) { Http::assertSent(function ($httpRequest) use ($data) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/login' return $httpRequest->url() === 'https://lemmy.world/api/v3/login'
&& $httpRequest->method() === 'POST' && $httpRequest->method() === 'POST'
@ -223,13 +224,13 @@ public function test_post_with_data(): void
public function test_post_with_http_scheme(): void public function test_post_with_http_scheme(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$request->withScheme('http'); $request->withScheme('http');
$response = $request->post('login'); $response = $request->post('login');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'http://lemmy.world/api/v3/login'; return $httpRequest->url() === 'http://lemmy.world/api/v3/login';
}); });
@ -238,10 +239,10 @@ public function test_post_with_http_scheme(): void
public function test_requests_use_30_second_timeout(): void public function test_requests_use_30_second_timeout(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$request->get('site'); $request->get('site');
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/site'; return $httpRequest->url() === 'https://lemmy.world/api/v3/site';
}); });
@ -250,12 +251,12 @@ public function test_requests_use_30_second_timeout(): void
public function test_chaining_methods(): void public function test_chaining_methods(): void
{ {
Http::fake(['*' => Http::response(['success' => true])]); Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world'); $request = new LemmyRequest('lemmy.world');
$response = $request->withScheme('http')->withToken('chained-token')->get('site'); $response = $request->withScheme('http')->withToken('chained-token')->get('site');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) { Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'http://lemmy.world/api/v3/site' return $httpRequest->url() === 'http://lemmy.world/api/v3/site'
&& $httpRequest->header('Authorization')[0] === 'Bearer chained-token'; && $httpRequest->header('Authorization')[0] === 'Bearer chained-token';
@ -267,7 +268,7 @@ private function getPrivateProperty(object $object, string $property): mixed
$reflection = new \ReflectionClass($object); $reflection = new \ReflectionClass($object);
$reflectionProperty = $reflection->getProperty($property); $reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true); $reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($object); return $reflectionProperty->getValue($object);
} }
} }

Some files were not shown because too many files have changed in this diff Show more