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

View file

@ -3,9 +3,6 @@
namespace App\Http\Controllers\Api\V1;
use App\Models\Article;
use App\Models\Feed;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Services\DashboardStatsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -40,8 +37,7 @@ public function stats(Request $request): JsonResponse
'current_period' => $period,
]);
} 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
// 2. They don't have all required components
$hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute;
$needsOnboarding = !$onboardingCompleted && !$onboardingSkipped && !$hasAllComponents;
$needsOnboarding = ! $onboardingCompleted && ! $onboardingSkipped && ! $hasAllComponents;
// Determine current step
$currentStep = null;
if ($needsOnboarding) {
if (!$hasPlatformAccount) {
if (! $hasPlatformAccount) {
$currentStep = 'platform';
} elseif (!$hasFeed) {
} elseif (! $hasFeed) {
$currentStep = 'feed';
} elseif (!$hasChannel) {
} elseif (! $hasChannel) {
$currentStep = 'channel';
} elseif (!$hasRoute) {
} elseif (! $hasRoute) {
$currentStep = 'route';
}
}
@ -56,7 +56,7 @@ public function status(): JsonResponse
'has_route' => $hasRoute,
'onboarding_skipped' => $onboardingSkipped,
'onboarding_completed' => $onboardingCompleted,
'missing_components' => !$hasAllComponents && $onboardingCompleted,
'missing_components' => ! $hasAllComponents && $onboardingCompleted,
], 'Onboarding status retrieved successfully.');
}
@ -84,8 +84,10 @@ public function options(): JsonResponse
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
// Get feed providers from config
$feedProviders = collect(config('feed.providers', []))
->filter(fn($provider) => $provider['is_active'])
/** @var array<string, array<string, mixed>> $providers */
$providers = config('feed.providers', []);
$feedProviders = collect($providers)
->filter(fn (array $provider) => $provider['is_active'])
->values();
return $this->sendResponse([

View file

@ -5,8 +5,8 @@
use App\Actions\CreateChannelAction;
use App\Http\Requests\StorePlatformChannelRequest;
use App\Http\Resources\PlatformChannelResource;
use App\Models\PlatformChannel;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -54,7 +54,7 @@ public function store(StorePlatformChannelRequest $request, CreateChannelAction
} catch (RuntimeException $e) {
return $this->sendError($e->getMessage(), [], 422);
} 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) {
return $this->sendValidationError($e->errors());
} 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!'
);
} 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
{
try {
$newStatus = !$channel->is_active;
$newStatus = ! $channel->is_active;
$channel->update(['is_active' => $newStatus]);
$status = $newStatus ? 'activated' : 'deactivated';
@ -128,7 +128,7 @@ public function toggle(PlatformChannel $channel): JsonResponse
"Platform channel {$status} successfully!"
);
} 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',
]);
/** @var PlatformAccount $platformAccount */
$platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']);
// Check if account is already attached
@ -165,7 +166,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
} catch (ValidationException $e) {
return $this->sendValidationError($e->errors());
} 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
{
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);
}
@ -186,7 +187,7 @@ public function detachAccount(PlatformChannel $channel, PlatformAccount $account
'Platform account detached from channel successfully!'
);
} 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',
]);
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);
}
@ -218,7 +219,7 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount
} catch (ValidationException $e) {
return $this->sendValidationError($e->errors());
} 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 Illuminate\Auth\Events\Verified;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
@ -19,7 +20,9 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse
}
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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,9 +8,16 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property integer $article_id
* @property integer $platform_channel_id
* @property integer $post_id
* @property int $id
* @property int $article_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)
*/
@ -18,7 +25,7 @@ class ArticlePublication extends Model
{
/** @use HasFactory<ArticlePublicationFactory> */
use HasFactory;
protected $fillable = [
'article_id',
'platform_channel_id',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,9 @@
*/
class PlatformChannelPost extends Model
{
/** @use HasFactory<\Illuminate\Database\Eloquent\Factories\Factory<PlatformChannelPost>> */
use HasFactory;
protected $fillable = [
'platform',
'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
{
if (!$url && !$title) {
if (! $url && ! $title) {
return false;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,13 +2,15 @@
namespace App\Modules\Lemmy;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
class LemmyRequest
{
private string $instance;
private ?string $token;
private string $scheme = 'https';
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)) {
$this->scheme = $scheme;
}
return $this;
}
/**
* @param array<string, mixed> $params
* @param array<string, mixed> $params
*/
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
{
@ -83,6 +86,7 @@ public function post(string $endpoint, array $data = []): Response
public function withToken(string $token): self
{
$this->token = $token;
return $this;
}
}

View file

@ -39,7 +39,7 @@ public function login(string $username, string $password): ?string
'password' => $password,
]);
if (!$response->successful()) {
if (! $response->successful()) {
$responseBody = $response->body();
logger()->error('Lemmy login failed', [
'status' => $response->status(),
@ -61,6 +61,7 @@ public function login(string $username, string $password): ?string
}
$data = $response->json();
return $data['jwt'] ?? null;
} catch (Exception $e) {
// Re-throw rate limit exceptions immediately
@ -74,7 +75,7 @@ public function login(string $username, string $password): ?string
continue;
}
// 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);
$response = $request->get('community', ['name' => $communityName]);
if (!$response->successful()) {
throw new Exception('Failed to fetch community: ' . $response->status());
if (! $response->successful()) {
throw new Exception('Failed to fetch community: '.$response->status());
}
$data = $response->json();
return $data['community_view']['community']['id'] ?? throw new Exception('Community not found');
} catch (Exception $e) {
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', [
'community_id' => $platformChannelId,
'limit' => 50,
'sort' => 'New'
'sort' => 'New',
]);
if (!$response->successful()) {
if (! $response->successful()) {
logger()->warning('Failed to sync channel posts', [
'status' => $response->status(),
'platform_channel_id' => $platformChannelId
'platform_channel_id' => $platformChannelId,
]);
return;
}
@ -137,13 +140,13 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
logger()->info('Synced channel posts', [
'platform_channel_id' => $platformChannelId,
'posts_count' => count($posts)
'posts_count' => count($posts),
]);
} catch (Exception $e) {
logger()->error('Exception while syncing channel posts', [
'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);
if (!$response->successful()) {
throw new Exception('Failed to create post: ' . $response->status() . ' - ' . $response->body());
if (! $response->successful()) {
throw new Exception('Failed to create post: '.$response->status().' - '.$response->body());
}
return $response->json();
@ -196,19 +199,22 @@ public function getLanguages(): array
$request = new LemmyRequest($this->instance);
$response = $request->get('site');
if (!$response->successful()) {
if (! $response->successful()) {
logger()->warning('Failed to fetch site languages', [
'status' => $response->status()
'status' => $response->status(),
]);
return [];
}
$data = $response->json();
return $data['all_languages'] ?? [];
} catch (Exception $e) {
logger()->error('Exception while fetching languages', [
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
return [];
}
}

View file

@ -12,6 +12,7 @@
class LemmyPublisher
{
private LemmyApiService $api;
private 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>
*
* @throws PlatformAuthException
* @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 (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
$token = $authService->refreshToken($this->account);
return $this->createPost($token, $extractedData, $channel, $article);
}
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>
*/
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
);
}
}

View file

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

View file

@ -12,23 +12,23 @@ public function __construct(
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);
// Update article with fetched metadata (title, description)
$updateData = [];
if (!empty($articleData)) {
if (! empty($articleData)) {
$updateData['title'] = $articleData['title'] ?? $article->title;
$updateData['description'] = $articleData['description'] ?? $article->description;
$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', [
'article_id' => $article->id,
'url' => $article->url
'url' => $article->url,
]);
$updateData['approval_status'] = 'rejected';
@ -67,7 +67,7 @@ private function validateByKeywords(string $full_article): bool
// Common Belgian news topics
'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) {

View file

@ -32,14 +32,14 @@ public function getToken(PlatformAccount $account): string
public function refreshToken(PlatformAccount $account): string
{
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);
$token = $api->login($account->username, $account->password);
if (!$token) {
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account: ' . $account->username);
if (! $token) {
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account: '.$account->username);
}
// 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
*
* @throws PlatformAuthException
*/
/**
* @return array<string, mixed>
*/
public function authenticate(string $instanceUrl, string $username, string $password): array
{
try {
$api = new LemmyApiService($instanceUrl);
$token = $api->login($username, $password);
if (!$token) {
if (! $token) {
// Throw a clean exception that will be caught and handled by the controller
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
'display_name' => null,
'bio' => null,
]
]
],
],
];
} catch (PlatformAuthException $e) {
// Re-throw PlatformAuthExceptions as-is to avoid nesting

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
class LogSaver
{
/**
* @param array<string, mixed> $context
* @param array<string, mixed> $context
*/
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
{
@ -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
{
@ -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
{
@ -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
{

View file

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

View file

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

View file

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

View file

@ -7,9 +7,14 @@
class BelgaHomepageParserAdapter implements HomepageParserInterface
{
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
{
return str_contains($url, 'belganewsagency.eu');
@ -29,4 +34,4 @@ public function getSourceName(): string
{
return 'Belga News Agency';
}
}
}

View file

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

View file

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

View file

@ -48,13 +48,13 @@ public static function extractFullArticle(string $html): ?string
// Extract all paragraph content
preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches);
if (!empty($matches[1])) {
$paragraphs = array_map(function($paragraph) {
if (! empty($matches[1])) {
$paragraphs = array_map(function ($paragraph) {
return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8');
}, $matches[1]);
// 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) !== '';
}));

View file

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

View file

@ -10,13 +10,13 @@ class VrtHomepageParser
public static function extractArticleUrls(string $html, string $language = 'en'): array
{
$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])
->unique()
->map(fn ($path) => 'https://www.vrt.be' . $path)
->map(fn ($path) => 'https://www.vrt.be'.$path)
->toArray();
return $urls;
}
}
}

View file

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

View file

@ -12,15 +12,13 @@
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Log\LogSaver;
use Exception;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use RuntimeException;
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)
*/
@ -28,9 +26,11 @@ protected function makePublisher(mixed $account): LemmyPublisher
{
return new LemmyPublisher($account);
}
/**
* @param array<string, mixed> $extractedData
* @param array<string, mixed> $extractedData
* @return Collection<int, ArticlePublication>
*
* @throws PublishException
*/
public function publishToRoutedChannels(Article $article, array $extractedData): Collection
@ -60,7 +60,7 @@ public function publishToRoutedChannels(Article $article, array $extractedData):
if (! $account) {
$this->logSaver->warning('No active account for channel', $channel, [
'article_id' => $article->id,
'route_priority' => $route->priority
'route_priority' => $route->priority,
]);
return null;
@ -68,12 +68,13 @@ public function publishToRoutedChannels(Article $article, array $extractedData):
return $this->publishToChannel($article, $extractedData, $channel, $account);
})
->filter();
->filter();
}
/**
* 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
{
@ -91,10 +92,10 @@ private function routeMatchesArticle(Route $route, array $extractedData): bool
$articleContent = $extractedData['full_article'];
}
if (isset($extractedData['title'])) {
$articleContent .= ' ' . $extractedData['title'];
$articleContent .= ' '.$extractedData['title'];
}
if (isset($extractedData['description'])) {
$articleContent .= ' ' . $extractedData['description'];
$articleContent .= ' '.$extractedData['description'];
}
// 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
{
@ -145,14 +146,14 @@ private function publishToChannel(Article $article, array $extractedData, Platfo
]);
$this->logSaver->info('Published to channel via keyword-filtered routing', $channel, [
'article_id' => $article->id
'article_id' => $article->id,
]);
return $publication;
} catch (Exception $e) {
$this->logSaver->warning('Failed to publish to channel', $channel, [
'article_id' => $article->id,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
return null;

View file

@ -10,7 +10,8 @@
class RoutingValidationService
{
/**
* @param Collection<int, PlatformChannel> $channels
* @param Collection<int, PlatformChannel> $channels
*
* @throws RoutingMismatchException
*/
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;
use App\Models\Feed;
use App\Models\Route;
use App\Models\PlatformChannel;
use App\Models\Route;
use App\Models\Setting;
class SystemStatusService
@ -17,22 +17,22 @@ public function getSystemStatus(): array
$reasons = [];
$isEnabled = true;
if (!Setting::isArticleProcessingEnabled()) {
if (! Setting::isArticleProcessingEnabled()) {
$isEnabled = false;
$reasons[] = 'Manually disabled by user';
}
if (!Feed::where('is_active', true)->exists()) {
if (! Feed::where('is_active', true)->exists()) {
$isEnabled = false;
$reasons[] = 'No active feeds configured';
}
if (!PlatformChannel::where('is_active', true)->exists()) {
if (! PlatformChannel::where('is_active', true)->exists()) {
$isEnabled = false;
$reasons[] = 'No active platform channels configured';
}
if (!Route::where('is_active', true)->exists()) {
if (! Route::where('is_active', true)->exists()) {
$isEnabled = false;
$reasons[] = 'No active feed-to-channel routes configured';
}
@ -49,4 +49,4 @@ public function canProcessArticles(): bool
{
return $this->getSystemStatus()['is_enabled'];
}
}
}

View file

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

View file

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

View file

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

View file

@ -2,11 +2,6 @@
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 Tests\TestCase;
@ -31,7 +26,7 @@ public function test_api_routes_are_publicly_accessible(): void
'/api/v1/feeds',
'/api/v1/routing',
'/api/v1/settings',
'/api/v1/logs'
'/api/v1/logs',
];
foreach ($routes as $route) {
@ -49,7 +44,7 @@ public function test_fallback_route_returns_api_message(): void
$response->assertStatus(404);
$response->assertJson([
'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([
'name' => 'Test User',
'email' => 'test@example.com'
'email' => 'test@example.com',
]);
$this->assertDatabaseHas('users', [
'name' => 'Test User',
'email' => 'test@example.com'
'email' => 'test@example.com',
]);
$this->assertEquals('Test User', $user->name);
@ -43,38 +43,40 @@ public function test_language_model_creates_successfully(): void
{
$language = Language::factory()->create([
'name' => 'English',
'short_code' => 'en'
'short_code' => 'en',
]);
$this->assertDatabaseHas('languages', [
'name' => 'English',
'short_code' => 'en'
'short_code' => 'en',
]);
}
public function test_platform_instance_model_creates_successfully(): void
{
/** @var PlatformInstance $instance */
$instance = PlatformInstance::factory()->create([
'name' => 'Test Instance',
'url' => 'https://test.lemmy.world'
'url' => 'https://test.lemmy.world',
]);
$this->assertDatabaseHas('platform_instances', [
'name' => 'Test Instance',
'url' => 'https://test.lemmy.world'
'url' => 'https://test.lemmy.world',
]);
}
public function test_platform_account_model_creates_successfully(): void
{
/** @var PlatformAccount $account */
$account = PlatformAccount::factory()->create([
'username' => 'testuser',
'is_active' => true
'is_active' => true,
]);
$this->assertDatabaseHas('platform_accounts', [
'username' => 'testuser',
'is_active' => true
'is_active' => true,
]);
$this->assertEquals('testuser', $account->username);
@ -84,19 +86,19 @@ public function test_platform_channel_model_creates_successfully(): void
{
$language = Language::factory()->create();
$instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id,
'name' => 'Test Channel',
'is_active' => true
'is_active' => true,
]);
$this->assertDatabaseHas('platform_channels', [
'platform_instance_id' => $instance->id,
'language_id' => $language->id,
'name' => 'Test Channel',
'is_active' => true
'is_active' => true,
]);
$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
{
$language = Language::factory()->create();
$feed = Feed::factory()->create([
'language_id' => $language->id,
'name' => 'Test Feed',
'url' => 'https://example.com/feed.rss',
'is_active' => true
'is_active' => true,
]);
$this->assertDatabaseHas('feeds', [
'language_id' => $language->id,
'name' => 'Test Feed',
'url' => 'https://example.com/feed.rss',
'is_active' => true
'is_active' => true,
]);
$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
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'title' => 'Test Article',
'url' => 'https://example.com/article',
'approval_status' => 'pending'
'approval_status' => 'pending',
]);
$this->assertDatabaseHas('articles', [
'feed_id' => $feed->id,
'title' => 'Test Article',
'url' => 'https://example.com/article',
'approval_status' => 'pending'
'approval_status' => 'pending',
]);
$this->assertEquals($feed->id, $article->feed->id);
@ -149,20 +151,20 @@ public function test_article_publication_model_creates_successfully(): void
{
$article = Article::factory()->create();
$channel = PlatformChannel::factory()->create();
$publication = ArticlePublication::create([
'article_id' => $article->id,
'platform_channel_id' => $channel->id,
'post_id' => 'test-post-123',
'published_at' => now(),
'published_by' => 'test-user'
'published_by' => 'test-user',
]);
$this->assertDatabaseHas('article_publications', [
'article_id' => $article->id,
'platform_channel_id' => $channel->id,
'post_id' => 'test-post-123',
'published_by' => 'test-user'
'published_by' => 'test-user',
]);
$this->assertEquals($article->id, $publication->article->id);
@ -173,17 +175,18 @@ public function test_route_model_creates_successfully(): void
{
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create();
/** @var Route $route */
$route = Route::factory()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true
'is_active' => true,
]);
$this->assertDatabaseHas('routes', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true
'is_active' => true,
]);
$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
// Commenting out for now since the model works correctly
$this->assertTrue(true);
// $post = new PlatformChannelPost([
// 'platform' => PlatformEnum::LEMMY,
// 'channel_id' => 'technology',
// 'channel_id' => 'technology',
// 'post_id' => 'external-post-123',
// 'title' => 'Test Post',
// 'url' => 'https://example.com/post',
// 'posted_at' => now()
// ]);
// $post->save();
// $this->assertDatabaseHas('platform_channel_posts', [
@ -222,20 +225,20 @@ public function test_keyword_model_creates_successfully(): void
{
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create();
$keyword = Keyword::factory()
->forFeed($feed)
->forChannel($channel)
->create([
'keyword' => 'test keyword',
'is_active' => true
'is_active' => true,
]);
$this->assertDatabaseHas('keywords', [
'keyword' => 'test keyword',
'is_active' => true,
'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',
'message' => 'Test log message',
'context' => json_encode(['key' => 'value']),
'logged_at' => now()
'logged_at' => now(),
]);
$this->assertDatabaseHas('logs', [
'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([
'key' => 'test_setting',
'value' => 'test_value'
'value' => 'test_value',
]);
$this->assertDatabaseHas('settings', [
'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]);
$this->assertCount(3, $feed->articles);
foreach ($articles as $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();
$channel = PlatformChannel::factory()->create();
// Test the pivot table relationship
$account->channels()->attach($channel->id, ['is_active' => true, 'priority' => 1]);
$this->assertDatabaseHas('platform_account_channels', [
'platform_account_id' => $account->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();
$instances = PlatformInstance::factory()->count(2)->create();
// Attach language to instances via pivot table
foreach ($instances as $instance) {
$language->platformInstances()->attach($instance->id, ['platform_language_id' => rand(1, 100)]);
}
$this->assertCount(2, $language->platformInstances);
foreach ($instances as $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
$feed = Feed::factory()->create();
$feedId = $feed->id;
$feed->delete();
// Should not find with normal query if soft deleted
$this->assertNull(Feed::find($feedId));
// Should find with withTrashed if model uses soft deletes
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
$this->expectException(\Illuminate\Database\QueryException::class);
// Try to create article with non-existent feed_id
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);
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -38,7 +38,7 @@ public function test_stats_returns_successful_response(): void
'available_periods',
'current_period',
],
'message'
'message',
]);
}
@ -54,7 +54,7 @@ public function test_stats_with_different_periods(): void
'success' => true,
'data' => [
'current_period' => $period,
]
],
]);
}
}
@ -80,7 +80,7 @@ public function test_stats_with_sample_data(): void
ArticlePublication::factory()->create([
'article_id' => $articles->first()->id,
'platform_channel_id' => $channel->id,
'published_at' => now()
'published_at' => now(),
]);
$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_published' => $initialPublications + 1,
],
]
],
]);
// 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,
'active_routes' => 0,
],
]
],
]);
}
}

View file

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

View file

@ -14,21 +14,23 @@ class KeywordsControllerTest extends TestCase
use RefreshDatabase;
protected Feed $feed;
protected PlatformChannel $channel;
protected Route $route;
protected function setUp(): void
{
parent::setUp();
$this->feed = Feed::factory()->create();
$this->channel = PlatformChannel::factory()->create();
$this->route = Route::create([
'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id,
'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,
'platform_channel_id' => $this->channel->id,
'keyword' => 'test keyword',
'is_active' => true
'is_active' => true,
]);
$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',
'is_active',
'feed_id',
'platform_channel_id'
]
]
'platform_channel_id',
],
],
])
->assertJsonPath('data.0.keyword', 'test keyword');
}
@ -63,7 +65,7 @@ public function test_can_create_keyword_for_route(): void
{
$keywordData = [
'keyword' => 'new keyword',
'is_active' => true
'is_active' => true,
];
$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',
'is_active',
'feed_id',
'platform_channel_id'
]
'platform_channel_id',
],
])
->assertJsonPath('data.keyword', 'new keyword')
->assertJsonPath('data.is_active', true);
@ -86,7 +88,7 @@ public function test_can_create_keyword_for_route(): void
'keyword' => 'new keyword',
'feed_id' => $this->feed->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([
'feed_id' => $this->feed->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", [
'keyword' => 'duplicate keyword'
'keyword' => 'duplicate keyword',
]);
$response->assertStatus(409)
@ -109,14 +111,15 @@ public function test_cannot_create_duplicate_keyword_for_route(): void
public function test_can_update_keyword(): void
{
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([
'feed_id' => $this->feed->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}", [
'is_active' => false
'is_active' => false,
]);
$response->assertStatus(200)
@ -124,15 +127,16 @@ public function test_can_update_keyword(): void
$this->assertDatabaseHas('keywords', [
'id' => $keyword->id,
'is_active' => false
'is_active' => false,
]);
}
public function test_can_delete_keyword(): void
{
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([
'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}");
@ -140,16 +144,17 @@ public function test_can_delete_keyword(): void
$response->assertStatus(200);
$this->assertDatabaseMissing('keywords', [
'id' => $keyword->id
'id' => $keyword->id,
]);
}
public function test_can_toggle_keyword(): void
{
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([
'feed_id' => $this->feed->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");
@ -159,7 +164,7 @@ public function test_can_toggle_keyword(): void
$this->assertDatabaseHas('keywords', [
'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();
$otherChannel = PlatformChannel::factory()->create();
/** @var Keyword $keyword */
$keyword = Keyword::factory()->create([
'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}");
@ -186,4 +192,4 @@ public function test_validates_required_fields(): void
$response->assertStatus(422)
->assertJsonValidationErrors(['keyword']);
}
}
}

View file

@ -37,7 +37,7 @@ public function test_index_returns_successful_response(): void
'context',
'created_at',
'updated_at',
]
],
],
'pagination' => [
'current_page',
@ -46,12 +46,12 @@ public function test_index_returns_successful_response(): void
'total',
'from',
'to',
]
]
],
],
])
->assertJson([
'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->assertStatus(200);
$logs = $response->json('data.logs');
$this->assertEquals($newestLog->id, $logs[0]['id']);
$this->assertEquals($newLog->id, $logs[1]['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->assertStatus(200);
$logs = $response->json('data.logs');
$this->assertCount(1, $logs);
$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->assertStatus(200);
$logs = $response->json('data.logs');
$pagination = $response->json('data.pagination');
$this->assertCount(5, $logs);
$this->assertEquals(5, $pagination['per_page']);
$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->assertStatus(200);
$pagination = $response->json('data.pagination');
// Should be limited to 100 as per controller logic
$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->assertStatus(200);
$pagination = $response->json('data.pagination');
// Should use default of 20
$this->assertEquals(20, $pagination['per_page']);
}
@ -147,8 +147,8 @@ public function test_index_handles_empty_logs(): void
'total' => 0,
'current_page' => 1,
'last_page' => 1,
]
]
],
],
]);
}
@ -159,7 +159,7 @@ public function test_index_pagination_works_correctly(): void
// Test first page
$response = $this->getJson('/api/v1/logs?per_page=10&page=1');
$response->assertStatus(200);
$pagination = $response->json('data.pagination');
$this->assertEquals(1, $pagination['current_page']);
$this->assertEquals(3, $pagination['last_page']);
@ -169,7 +169,7 @@ public function test_index_pagination_works_correctly(): void
// Test second page
$response = $this->getJson('/api/v1/logs?per_page=10&page=2');
$response->assertStatus(200);
$pagination = $response->json('data.pagination');
$this->assertEquals(2, $pagination['current_page']);
$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->assertStatus(200);
$logs = $response->json('data.logs');
$this->assertCount(4, $logs);
$levels = array_column($logs, 'level');
$this->assertContains('error', $levels);
$this->assertContains('warning', $levels);
$this->assertContains('info', $levels);
$this->assertContains('debug', $levels);
}
}
}

View file

@ -19,7 +19,7 @@ class OnboardingControllerTest extends TestCase
protected function setUp(): void
{
parent::setUp();
// Create a language for testing
Language::factory()->create([
'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');
@ -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]);
@ -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();
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();
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();
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
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([
'platform' => 'lemmy',
@ -179,34 +179,34 @@ public function test_options_returns_languages_and_platform_instances()
'success',
'data' => [
'languages' => [
'*' => ['id', 'short_code', 'name', 'native_name', 'is_active']
'*' => ['id', 'short_code', 'name', 'native_name', 'is_active'],
],
'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->assertStatus(200)
->assertJson([
'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->assertStatus(200)
->assertJson([
'success' => true,
'data' => ['skipped' => true]
'data' => ['skipped' => true],
]);
$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
Setting::create([
@ -236,7 +236,7 @@ public function test_skip_onboarding_updates_existing_setting()
$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
Setting::create([
@ -249,7 +249,7 @@ public function test_reset_skip_removes_setting()
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => ['reset' => true]
'data' => ['reset' => true],
]);
$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->assertStatus(200)
->assertJson([
'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
$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->assertJson(['data' => ['needs_onboarding' => true, 'onboarding_skipped' => false]]);
}
}
}

View file

@ -33,12 +33,12 @@ public function test_index_returns_successful_response(): void
'is_active',
'created_at',
'updated_at',
]
]
],
],
])
->assertJson([
'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',
'created_at',
'updated_at',
]
],
])
->assertJson([
'success' => true,
'message' => 'Platform account created successfully!'
'message' => 'Platform account created successfully!',
]);
$this->assertDatabaseHas('platform_accounts', [
@ -115,7 +115,7 @@ public function test_show_returns_platform_account_successfully(): void
'is_active',
'created_at',
'updated_at',
]
],
])
->assertJson([
'success' => true,
@ -123,7 +123,7 @@ public function test_show_returns_platform_account_successfully(): void
'data' => [
'id' => $account->id,
'username' => $account->username,
]
],
]);
}
@ -134,7 +134,7 @@ public function test_update_modifies_platform_account_successfully(): void
$updateData = [
'instance_url' => 'https://updated.example.com',
'username' => 'updateduser',
'settings' => ['updated' => 'value']
'settings' => ['updated' => 'value'],
];
$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)
->assertJson([
'success' => true,
'message' => 'Platform account updated successfully!'
'message' => 'Platform account updated successfully!',
]);
$this->assertDatabaseHas('platform_accounts', [
@ -161,11 +161,11 @@ public function test_destroy_deletes_platform_account_successfully(): void
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Platform account deleted successfully!'
'message' => 'Platform account deleted successfully!',
]);
$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', [
'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([
'platform' => 'lemmy',
'is_active' => true
'is_active' => true,
]);
$newAccount = PlatformAccount::factory()->create([
'platform' => 'lemmy',
'is_active' => false
'is_active' => false,
]);
$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', [
'id' => $activeAccount->id,
'is_active' => false
'is_active' => false,
]);
$this->assertDatabaseHas('platform_accounts', [
'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',
'created_at',
'updated_at',
'platform_instance'
]
]
'platform_instance',
],
],
])
->assertJson([
'success' => true,
'message' => 'Platform channels retrieved successfully.'
'message' => 'Platform channels retrieved successfully.',
]);
}
public function test_store_creates_platform_channel_successfully(): void
{
$instance = PlatformInstance::factory()->create();
// Create a platform account for this instance first
PlatformAccount::factory()->create([
'instance_url' => $instance->url,
'is_active' => true
'is_active' => true,
]);
$data = [
@ -76,11 +76,11 @@ public function test_store_creates_platform_channel_successfully(): void
'is_active',
'created_at',
'updated_at',
]
],
])
->assertJson([
'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', [
@ -102,7 +102,7 @@ public function test_store_validates_platform_instance_exists(): void
{
$data = [
'platform_instance_id' => 999,
'name' => 'Test Channel'
'name' => 'Test Channel',
];
$response = $this->postJson('/api/v1/platform-channels', $data);
@ -132,8 +132,8 @@ public function test_show_returns_platform_channel_successfully(): void
'is_active',
'created_at',
'updated_at',
'platform_instance'
]
'platform_instance',
],
])
->assertJson([
'success' => true,
@ -141,7 +141,7 @@ public function test_show_returns_platform_channel_successfully(): void
'data' => [
'id' => $channel->id,
'name' => $channel->name,
]
],
]);
}
@ -154,7 +154,7 @@ public function test_update_modifies_platform_channel_successfully(): void
'name' => 'Updated Channel',
'display_name' => 'Updated Display Name',
'description' => 'Updated description',
'is_active' => false
'is_active' => false,
];
$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)
->assertJson([
'success' => true,
'message' => 'Platform channel updated successfully!'
'message' => 'Platform channel updated successfully!',
]);
$this->assertDatabaseHas('platform_channels', [
@ -183,11 +183,11 @@ public function test_destroy_deletes_platform_channel_successfully(): void
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Platform channel deleted successfully!'
'message' => 'Platform channel deleted successfully!',
]);
$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();
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'is_active' => false
'is_active' => false,
]);
$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)
->assertJson([
'success' => true,
'message' => 'Platform channel activated successfully!'
'message' => 'Platform channel activated successfully!',
]);
$this->assertDatabaseHas('platform_channels', [
'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();
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'is_active' => true
'is_active' => true,
]);
$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)
->assertJson([
'success' => true,
'message' => 'Platform channel deactivated successfully!'
'message' => 'Platform channel deactivated successfully!',
]);
$this->assertDatabaseHas('platform_channels', [
'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();
$instance = PlatformInstance::factory()->create();
// Create unique feeds and channels for this test
$feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]);
$channels = PlatformChannel::factory()->count(3)->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
foreach ($feeds as $index => $feed) {
Route::factory()->create([
'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',
'created_at',
'updated_at',
]
]
],
],
])
->assertJson([
'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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$data = [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 5
'priority' => 5,
];
$response = $this->postJson('/api/v1/routing', $data);
@ -86,11 +86,11 @@ public function test_store_creates_routing_configuration_successfully(): void
'priority',
'created_at',
'updated_at',
]
],
])
->assertJson([
'success' => true,
'message' => 'Routing configuration created successfully!'
'message' => 'Routing configuration created successfully!',
]);
$this->assertDatabaseHas('routes', [
@ -115,12 +115,12 @@ public function test_store_validates_feed_exists(): void
$instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$data = [
'feed_id' => 999,
'platform_channel_id' => $channel->id
'platform_channel_id' => $channel->id,
];
$response = $this->postJson('/api/v1/routing', $data);
@ -136,7 +136,7 @@ public function test_store_validates_platform_channel_exists(): void
$data = [
'feed_id' => $feed->id,
'platform_channel_id' => 999
'platform_channel_id' => 999,
];
$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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$route = Route::factory()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id
'platform_channel_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',
'created_at',
'updated_at',
]
],
])
->assertJson([
'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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->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)
->assertJson([
'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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$route = Route::factory()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 1
'priority' => 1,
]);
$updateData = [
'is_active' => false,
'priority' => 10
'priority' => 10,
];
$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)
->assertJson([
'success' => true,
'message' => 'Routing configuration updated successfully!'
'message' => 'Routing configuration updated successfully!',
]);
$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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
'is_active' => false
'is_active' => false,
]);
$response->assertStatus(404)
->assertJson([
'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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$route = Route::factory()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id
'platform_channel_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)
->assertJson([
'success' => true,
'message' => 'Routing configuration deleted successfully!'
'message' => 'Routing configuration deleted successfully!',
]);
$this->assertDatabaseMissing('routes', [
'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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->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)
->assertJson([
'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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$route = Route::factory()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => false
'is_active' => false,
]);
$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)
->assertJson([
'success' => true,
'message' => 'Routing configuration activated successfully!'
'message' => 'Routing configuration activated successfully!',
]);
$this->assertDatabaseHas('routes', [
'feed_id' => $feed->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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$route = Route::factory()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true
'is_active' => true,
]);
$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)
->assertJson([
'success' => true,
'message' => 'Routing configuration deactivated successfully!'
'message' => 'Routing configuration deactivated successfully!',
]);
$this->assertDatabaseHas('routes', [
'feed_id' => $feed->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]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'language_id' => $language->id
'language_id' => $language->id,
]);
$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)
->assertJson([
'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',
'article_publishing_interval',
],
'message'
'message',
])
->assertJson([
'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.',
'data' => [
'article_processing_enabled' => false,
]
],
]);
}
@ -58,7 +58,7 @@ public function test_update_modifies_publishing_approvals_setting(): void
'message' => 'Settings updated successfully.',
'data' => [
'publishing_approvals_enabled' => true,
]
],
]);
}
@ -72,7 +72,7 @@ public function test_update_validates_boolean_values(): void
$response->assertStatus(422)
->assertJsonValidationErrors([
'article_processing_enabled',
'publishing_approvals_enabled'
'publishing_approvals_enabled',
]);
}
@ -88,7 +88,7 @@ public function test_update_accepts_partial_updates(): void
'success' => true,
'data' => [
'article_processing_enabled' => true,
]
],
]);
// Should still have structure for all settings
@ -97,7 +97,7 @@ public function test_update_accepts_partial_updates(): void
'article_processing_enabled',
'publishing_approvals_enabled',
'article_publishing_interval',
]
],
]);
}

View file

@ -18,11 +18,10 @@
use App\Models\Article;
use App\Models\Feed;
use App\Models\Log;
use App\Models\Setting;
use App\Models\PlatformChannel;
use App\Services\Log\LogSaver;
use App\Models\Setting;
use App\Services\Article\ArticleFetcher;
use App\Services\Article\ValidationService;
use App\Services\Log\LogSaver;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
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]);
$logSaver = app(LogSaver::class);
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
$job->handle($logSaver);
// Should dispatch individual feed jobs
@ -57,7 +56,7 @@ public function test_article_discovery_for_feed_job_processes_feed(): void
$feed = Feed::factory()->create([
'url' => 'https://example.com/feed',
'is_active' => true
'is_active' => true,
]);
// Mock the ArticleFetcher service in the container
@ -94,10 +93,9 @@ public function test_sync_channel_posts_job_processes_successfully(): void
$this->assertTrue(true);
}
public function test_publish_next_article_job_has_correct_configuration(): void
{
$job = new PublishNextArticleJob();
$job = new PublishNextArticleJob;
$this->assertEquals('publishing', $job->queue);
$this->assertInstanceOf(PublishNextArticleJob::class, $job);
@ -164,12 +162,12 @@ public function test_exception_logged_event_is_dispatched(): void
$log = Log::factory()->create([
'level' => 'error',
'message' => 'Test error',
'context' => json_encode(['key' => 'value'])
'context' => json_encode(['key' => 'value']),
]);
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';
});
}
@ -185,7 +183,7 @@ public function test_validate_article_listener_processes_new_article(): void
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'pending',
]);
]);
// Mock ArticleFetcher to return valid article data
$mockFetcher = \Mockery::mock(\App\Services\Article\ArticleFetcher::class);
@ -195,7 +193,7 @@ public function test_validate_article_listener_processes_new_article(): void
->andReturn([
'title' => 'Belgian News',
'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);
@ -248,10 +246,10 @@ public function test_log_exception_to_database_listener_creates_log(): void
$log = Log::factory()->create([
'level' => 'error',
'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');
$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', [
'level' => 'error',
'message' => 'Test exception message'
'message' => 'Test exception message',
]);
$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
{
$job = new PublishNextArticleJob();
$job = new PublishNextArticleJob;
// Test that job has unique configuration
$this->assertObjectHasProperty('uniqueFor', $job);
@ -300,9 +298,9 @@ public function test_job_queue_configuration(): void
$channel = PlatformChannel::factory()->create();
$article = Article::factory()->create(['feed_id' => $feed->id]);
$discoveryJob = new ArticleDiscoveryJob();
$discoveryJob = new ArticleDiscoveryJob;
$feedJob = new ArticleDiscoveryForFeedJob($feed);
$publishJob = new PublishNextArticleJob();
$publishJob = new PublishNextArticleJob;
$syncJob = new SyncChannelPostsJob($channel);
// Test queue assignments

View file

@ -2,7 +2,7 @@
namespace Tests\Feature;
use App\Events\ArticleReadyToPublish;
use App\Events\ArticleApproved;
use App\Events\NewArticleFetched;
use App\Listeners\ValidateArticleListener;
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
{
Event::fake([ArticleReadyToPublish::class]);
Event::fake([ArticleApproved::class]);
// Mock HTTP requests
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();
@ -42,17 +42,17 @@ public function test_listener_validates_article_and_dispatches_ready_to_publish_
$article->refresh();
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;
});
} else {
Event::assertNotDispatched(ArticleReadyToPublish::class);
Event::assertNotDispatched(ArticleApproved::class);
}
}
public function test_listener_skips_already_validated_articles(): void
{
Event::fake([ArticleReadyToPublish::class]);
Event::fake([ArticleApproved::class]);
$feed = Feed::factory()->create();
$article = Article::factory()->create([
@ -66,12 +66,12 @@ public function test_listener_skips_already_validated_articles(): void
$listener->handle($event);
Event::assertNotDispatched(ArticleReadyToPublish::class);
Event::assertNotDispatched(ArticleApproved::class);
}
public function test_listener_skips_articles_with_existing_publication(): void
{
Event::fake([ArticleReadyToPublish::class]);
Event::fake([ArticleApproved::class]);
$feed = Feed::factory()->create();
$article = Article::factory()->create([
@ -93,16 +93,16 @@ public function test_listener_skips_articles_with_existing_publication(): void
$listener->handle($event);
Event::assertNotDispatched(ArticleReadyToPublish::class);
Event::assertNotDispatched(ArticleApproved::class);
}
public function test_listener_calls_validation_service(): void
{
Event::fake([ArticleReadyToPublish::class]);
Event::fake([ArticleApproved::class]);
// Mock HTTP requests
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();

View file

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

View file

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

View file

@ -19,7 +19,7 @@ class CreateChannelActionTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->action = new CreateChannelAction();
$this->action = new CreateChannelAction;
}
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
$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
@ -57,7 +57,7 @@ public function test_creates_channel_without_language(): void
$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

View file

@ -17,7 +17,7 @@ class CreateFeedActionTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->action = new CreateFeedAction();
$this->action = new CreateFeedAction;
}
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('website', $feed->type);
$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
@ -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('rss', $feed->type);
$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

View file

@ -17,6 +17,8 @@ class CreatePlatformAccountActionTest extends TestCase
use RefreshDatabase;
private CreatePlatformAccountAction $action;
/** @var LemmyAuthService&\Mockery\MockInterface */
private LemmyAuthService $lemmyAuthService;
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('A test bio', $account->settings['description']);
$this->assertEquals('test-jwt-token', $account->settings['api_token']);
$this->assertDatabaseHas('platform_instances', [
'url' => 'https://lemmy.world',
'platform' => 'lemmy',

View file

@ -19,7 +19,7 @@ class CreateRouteActionTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->action = new CreateRouteAction();
$this->action = new CreateRouteAction;
}
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);
$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->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
{
$cases = LogLevelEnum::cases();
$this->assertCount(5, $cases);
$this->assertContains(LogLevelEnum::DEBUG, $cases);
$this->assertContains(LogLevelEnum::INFO, $cases);
@ -83,8 +83,8 @@ public function test_enum_can_be_compared(): void
$debug2 = LogLevelEnum::DEBUG;
$info = LogLevelEnum::INFO;
$this->assertTrue($debug1 === $debug2);
$this->assertFalse($debug1 === $info);
$this->assertTrue($debug1 === $debug2); // @phpstan-ignore identical.alwaysTrue
$this->assertFalse($debug1 === $info); // @phpstan-ignore identical.alwaysFalse, method.impossibleType
}
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('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
{
$cases = PlatformEnum::cases();
$this->assertCount(1, $cases);
$this->assertContains(PlatformEnum::LEMMY, $cases);
}
@ -54,7 +54,7 @@ public function test_enum_can_be_compared(): void
$lemmy1 = 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
@ -86,4 +86,4 @@ public function test_enum_value_is_string_backed(): void
{
$this->assertIsString(PlatformEnum::LEMMY->value);
}
}
}

View file

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

View file

@ -3,11 +3,11 @@
namespace Tests\Unit\Facades;
use App\Enums\LogLevelEnum;
use App\Enums\PlatformEnum;
use App\Facades\LogSaver;
use App\Models\Log;
use App\Models\PlatformChannel;
use App\Models\PlatformInstance;
use App\Enums\PlatformEnum;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@ -20,7 +20,7 @@ public function test_facade_accessor_returns_correct_service(): void
$reflection = new \ReflectionClass(LogSaver::class);
$method = $reflection->getMethod('getFacadeAccessor');
$method->setAccessible(true);
$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([
'platform' => PlatformEnum::LEMMY,
'url' => 'https://facade.test.com'
'url' => 'https://facade.test.com',
]);
$channel = PlatformChannel::factory()->create([
'name' => 'Facade Test Channel',
'platform_instance_id' => $platformInstance->id
'platform_instance_id' => $platformInstance->id,
]);
$message = 'Facade channel test';
@ -125,11 +125,11 @@ public function test_facade_static_calls_resolve_to_service_instance(): void
LogSaver::error('Test message 2');
$this->assertDatabaseCount('logs', 2);
$logs = Log::orderBy('id')->get();
$this->assertEquals('Test message 1', $logs[0]->message);
$this->assertEquals('Test message 2', $logs[1]->message);
$this->assertEquals(LogLevelEnum::INFO, $logs[0]->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();
$job = new ArticleDiscoveryForFeedJob($feed);
$this->assertEquals('feed-discovery', $job->queue);
}
@ -33,7 +33,7 @@ public function test_job_implements_should_queue(): void
{
$feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed);
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
}
@ -41,7 +41,7 @@ public function test_job_uses_queueable_trait(): void
{
$feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed);
$this->assertContains(
\Illuminate\Foundation\Queue\Queueable::class,
class_uses($job)
@ -54,7 +54,7 @@ public function test_handle_fetches_articles_and_updates_feed(): void
$feed = Feed::factory()->create([
'name' => 'Test Feed',
'url' => 'https://example.com/feed',
'last_fetched_at' => null
'last_fetched_at' => null,
]);
$mockArticles = collect(['article1', 'article2']);
@ -72,15 +72,15 @@ public function test_handle_fetches_articles_and_updates_feed(): void
->with('Starting feed article fetch', null, [
'feed_id' => $feed->id,
'feed_name' => $feed->name,
'feed_url' => $feed->url
'feed_url' => $feed->url,
])
->once();
$logSaverMock->shouldReceive('info')
->with('Feed article fetch completed', null, [
'feed_id' => $feed->id,
'feed_name' => $feed->name,
'articles_count' => 2
'articles_count' => 2,
])
->once();
@ -106,7 +106,7 @@ public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay():
$logSaverMock->shouldReceive('info')
->times(3) // Once for each active feed
->with('Dispatched feed discovery job', null, Mockery::type('array'));
$this->app->instance(LogSaver::class, $logSaverMock);
// Act
@ -114,7 +114,7 @@ public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay():
// Assert
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 3);
// 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
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->times(2);
$this->app->instance(LogSaver::class, $logSaverMock);
// Act
@ -134,7 +134,7 @@ public function test_dispatch_for_all_active_feeds_applies_correct_delays(): voi
// Assert
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 2);
// Verify jobs are pushed with delays
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, function ($job) {
return $job->delay !== null;
@ -157,7 +157,7 @@ public function test_feed_discovery_delay_constant_exists(): void
{
$reflection = new \ReflectionClass(ArticleDiscoveryForFeedJob::class);
$constant = $reflection->getConstant('FEED_DISCOVERY_DELAY_MINUTES');
$this->assertEquals(5, $constant);
}
@ -165,10 +165,10 @@ public function test_job_can_be_serialized(): void
{
$feed = Feed::factory()->create(['name' => 'Test Feed']);
$job = new ArticleDiscoveryForFeedJob($feed);
$serialized = serialize($job);
$unserialized = unserialize($serialized);
$this->assertInstanceOf(ArticleDiscoveryForFeedJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue);
// 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
$feed = Feed::factory()->create([
'name' => 'Test Feed',
'url' => 'https://example.com/feed'
'url' => 'https://example.com/feed',
]);
$mockArticles = collect([]);
@ -197,10 +197,10 @@ public function test_handle_logs_start_message_with_correct_context(): void
->with('Starting feed article fetch', null, [
'feed_id' => $feed->id,
'feed_name' => 'Test Feed',
'feed_url' => 'https://example.com/feed'
'feed_url' => 'https://example.com/feed',
])
->once();
$logSaverMock->shouldReceive('info')
->with('Feed article fetch completed', null, Mockery::type('array'))
->once();
@ -219,4 +219,4 @@ protected function tearDown(): void
Mockery::close();
parent::tearDown();
}
}
}

View file

@ -23,7 +23,7 @@ protected function setUp(): void
public function test_constructor_sets_correct_queue(): void
{
// Act
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Assert
$this->assertEquals('feed-discovery', $job->queue);
@ -40,7 +40,7 @@ public function test_handle_skips_when_article_processing_disabled(): void
->once()
->with('Article processing is disabled. Article discovery skipped.');
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Act
$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')
->once();
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Act
$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')
->once();
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Act
$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
{
// Arrange
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Assert
$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
{
// Arrange
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Assert
$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')
->once();
$job = new ArticleDiscoveryJob();
$job = new ArticleDiscoveryJob;
// Act - Should not throw any exceptions
$job->handle($logSaverMock);

View file

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

View file

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

View file

@ -15,21 +15,22 @@ class ArticlePublicationTest extends TestCase
public function test_fillable_fields(): void
{
$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());
}
public function test_table_name(): void
{
$publication = new ArticlePublication();
$publication = new ArticlePublication;
$this->assertEquals('article_publications', $publication->getTable());
}
public function test_casts_published_at_to_datetime(): void
{
$timestamp = now()->subHours(2);
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['published_at' => $timestamp]);
$this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at);
@ -43,11 +44,12 @@ public function test_casts_publication_data_to_array(): void
'platform_response' => [
'id' => 123,
'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]);
$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
{
$article = Article::factory()->create();
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['article_id' => $article->id]);
$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
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create();
$this->assertInstanceOf(ArticlePublication::class, $publication);
@ -90,7 +94,7 @@ public function test_publication_creation_with_explicit_values(): void
'published_at' => $publishedAt,
'published_by' => 'test_bot',
'platform' => 'lemmy',
'publication_data' => $publicationData
'publication_data' => $publicationData,
]);
$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
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->recentlyPublished()->create();
$this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at);
$this->assertTrue($publication->published_at->isAfter(now()->subDay()));
$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
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create([
'post_id' => 'original-id',
'published_by' => 'original_user'
'published_by' => 'original_user',
]);
$publication->update([
'post_id' => 'updated-id',
'published_by' => 'updated_user'
'published_by' => 'updated_user',
]);
$publication->refresh();
@ -131,6 +137,7 @@ public function test_publication_update(): void
public function test_publication_deletion(): void
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create();
$publicationId = $publication->id;
@ -141,6 +148,7 @@ public function test_publication_deletion(): void
public function test_publication_data_can_be_empty_array(): void
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['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
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => null]);
$this->assertNull($publication->publication_data);
@ -164,21 +173,22 @@ public function test_publication_data_can_be_complex_structure(): void
'author' => [
'id' => 456,
'name' => 'bot_user',
'display_name' => 'Bot User'
]
'display_name' => 'Bot User',
],
],
'metadata' => [
'retry_attempts' => 1,
'processing_time_ms' => 1250,
'error_log' => []
'error_log' => [],
],
'analytics' => [
'initial_views' => 0,
'initial_votes' => 0,
'engagement_tracked' => false
]
'engagement_tracked' => false,
],
];
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['publication_data' => $complexData]);
$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
{
$timestamp = now()->subHours(3);
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['published_at' => $timestamp]);
$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
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['published_by' => 'custom_bot']);
$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
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['platform' => 'lemmy']);
$this->assertEquals('lemmy', $publication->platform);
@ -212,6 +225,7 @@ public function test_publication_with_specific_platform(): void
public function test_publication_timestamps(): void
{
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create();
$this->assertNotNull($publication->created_at);
@ -226,16 +240,18 @@ public function test_multiple_publications_for_same_article(): void
$channel1 = PlatformChannel::factory()->create();
$channel2 = PlatformChannel::factory()->create();
/** @var ArticlePublication $publication1 */
$publication1 = ArticlePublication::factory()->create([
'article_id' => $article->id,
'platform_channel_id' => $channel1->id,
'post_id' => 'post-1'
'post_id' => 'post-1',
]);
/** @var ArticlePublication $publication2 */
$publication2 = ArticlePublication::factory()->create([
'article_id' => $article->id,
'platform_channel_id' => $channel2->id,
'post_id' => 'post-2'
'post_id' => 'post-2',
]);
$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
{
/** @var ArticlePublication $publication1 */
$publication1 = ArticlePublication::factory()->create(['platform' => 'lemmy']);
/** @var ArticlePublication $publication2 */
$publication2 = ArticlePublication::factory()->create(['platform' => 'lemmy']);
$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
{
/** @var ArticlePublication[] $publications */
$publications = [
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' => '12345']),
];
@ -275,15 +294,16 @@ public function test_publication_data_with_error_information(): void
'error' => [
'code' => 403,
'message' => 'Insufficient permissions',
'details' => 'Bot account lacks posting privileges'
'details' => 'Bot account lacks posting privileges',
],
'retry_info' => [
'max_retries' => 3,
'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]);
$this->assertEquals('failed', $publication->publication_data['status']);
@ -295,12 +315,13 @@ public function test_publication_relationship_with_article_data(): void
{
$article = Article::factory()->create([
'title' => 'Test Article Title',
'description' => 'Test article description'
'description' => 'Test article description',
]);
/** @var ArticlePublication $publication */
$publication = ArticlePublication::factory()->create(['article_id' => $article->id]);
$this->assertEquals('Test Article Title', $publication->article->title);
$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
Http::fake([
'*' => Http::response('', 500)
'*' => Http::response('', 500),
]);
// 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
{
$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());
}
public function test_casts_settings_to_array(): void
{
$settings = ['key1' => 'value1', 'key2' => ['nested' => 'value']];
$feed = Feed::factory()->create(['settings' => $settings]);
$this->assertIsArray($feed->settings);
@ -41,7 +41,7 @@ public function test_casts_is_active_to_boolean(): void
$feed->update(['is_active' => '0']);
$feed->refresh();
$this->assertIsBool($feed->is_active);
$this->assertFalse($feed->is_active);
}
@ -75,7 +75,7 @@ public function test_status_attribute_never_fetched(): void
{
$feed = Feed::factory()->create([
'is_active' => true,
'last_fetched_at' => null
'last_fetched_at' => null,
]);
$this->assertEquals('Never fetched', $feed->status);
@ -85,7 +85,7 @@ public function test_status_attribute_recently_fetched(): void
{
$feed = Feed::factory()->create([
'is_active' => true,
'last_fetched_at' => now()->subHour()
'last_fetched_at' => now()->subHour(),
]);
$this->assertEquals('Recently fetched', $feed->status);
@ -95,7 +95,7 @@ public function test_status_attribute_fetched_hours_ago(): void
{
$feed = Feed::factory()->create([
'is_active' => true,
'last_fetched_at' => now()->subHours(5)->startOfHour()
'last_fetched_at' => now()->subHours(5)->startOfHour(),
]);
$this->assertStringContainsString('Fetched', $feed->status);
@ -106,7 +106,7 @@ public function test_status_attribute_fetched_days_ago(): void
{
$feed = Feed::factory()->create([
'is_active' => true,
'last_fetched_at' => now()->subDays(3)
'last_fetched_at' => now()->subDays(3),
]);
$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
{
$feed = Feed::factory()->create();
$article1 = Article::factory()->create(['feed_id' => $feed->id]);
$article2 = Article::factory()->create(['feed_id' => $feed->id]);
// Create article for different feed
$otherFeed = Feed::factory()->create();
Article::factory()->create(['feed_id' => $otherFeed->id]);
@ -153,14 +153,14 @@ public function test_belongs_to_many_channels_relationship(): void
'feed_id' => $feed->id,
'platform_channel_id' => $channel1->id,
'is_active' => true,
'priority' => 100
'priority' => 100,
]);
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel2->id,
'is_active' => false,
'priority' => 50
'priority' => 50,
]);
$channels = $feed->channels;
@ -171,8 +171,8 @@ public function test_belongs_to_many_channels_relationship(): void
// Test pivot data
$channel1FromRelation = $channels->find($channel1->id);
$this->assertEquals(1, $channel1FromRelation->pivot->is_active);
$this->assertEquals(100, $channel1FromRelation->pivot->priority);
$this->assertEquals(1, $channel1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $channel1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
}
public function test_active_channels_relationship(): void
@ -187,21 +187,21 @@ public function test_active_channels_relationship(): void
'feed_id' => $feed->id,
'platform_channel_id' => $activeChannel1->id,
'is_active' => true,
'priority' => 100
'priority' => 100,
]);
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $activeChannel2->id,
'is_active' => true,
'priority' => 200
'priority' => 200,
]);
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $inactiveChannel->id,
'is_active' => false,
'priority' => 150
'priority' => 150,
]);
$activeChannels = $feed->activeChannels;
@ -226,7 +226,7 @@ public function test_feed_creation_with_factory(): void
$this->assertIsString($feed->url);
$this->assertIsString($feed->type);
// 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->assertIsArray($feed->settings);
}
@ -244,7 +244,7 @@ public function test_feed_creation_with_explicit_values(): void
'language_id' => $language->id,
'description' => 'Test description',
'settings' => $settings,
'is_active' => false
'is_active' => false,
]);
$this->assertEquals('Test Feed', $feed->name);
@ -260,12 +260,12 @@ public function test_feed_update(): void
{
$feed = Feed::factory()->create([
'name' => 'Original Name',
'is_active' => true
'is_active' => true,
]);
$feed->update([
'name' => 'Updated Name',
'is_active' => false
'is_active' => false,
]);
$feed->refresh();
@ -298,13 +298,13 @@ public function test_feed_settings_can_be_complex_structure(): void
'parsing' => [
'selector' => 'article.post',
'title_selector' => 'h1',
'content_selector' => '.content'
'content_selector' => '.content',
],
'filters' => ['min_length' => 100],
'schedule' => [
'enabled' => true,
'interval' => 3600
]
'interval' => 3600,
],
];
$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->updated_at);
}
}
}

View file

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

View file

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

View file

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

View file

@ -18,15 +18,15 @@ class PlatformChannelTest extends TestCase
public function test_fillable_fields(): void
{
$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());
}
public function test_table_name(): void
{
$channel = new PlatformChannel();
$channel = new PlatformChannel;
$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->refresh();
$this->assertIsBool($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
$channel->platformAccounts()->attach($account1->id, [
'is_active' => true,
'priority' => 100
'priority' => 100,
]);
$channel->platformAccounts()->attach($account2->id, [
'is_active' => false,
'priority' => 50
'priority' => 50,
]);
$accounts = $channel->platformAccounts;
@ -89,12 +89,12 @@ public function test_belongs_to_many_platform_accounts_relationship(): void
// Test pivot data
$account1FromRelation = $accounts->find($account1->id);
$this->assertEquals(1, $account1FromRelation->pivot->is_active);
$this->assertEquals(100, $account1FromRelation->pivot->priority);
$this->assertEquals(1, $account1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $account1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
$account2FromRelation = $accounts->find($account2->id);
$this->assertEquals(0, $account2FromRelation->pivot->is_active);
$this->assertEquals(50, $account2FromRelation->pivot->priority);
$this->assertEquals(0, $account2FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(50, $account2FromRelation->pivot->priority); // @phpstan-ignore property.notFound
}
public function test_active_platform_accounts_relationship(): void
@ -107,17 +107,17 @@ public function test_active_platform_accounts_relationship(): void
// Attach accounts
$channel->platformAccounts()->attach($activeAccount1->id, [
'is_active' => true,
'priority' => 100
'priority' => 100,
]);
$channel->platformAccounts()->attach($activeAccount2->id, [
'is_active' => true,
'priority' => 200
'priority' => 200,
]);
$channel->platformAccounts()->attach($inactiveAccount->id, [
'is_active' => false,
'priority' => 150
'priority' => 150,
]);
$activeAccounts = $channel->activePlatformAccounts;
@ -133,7 +133,7 @@ public function test_full_name_attribute(): void
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.example.com']);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'name' => 'technology'
'name' => 'technology',
]);
$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,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 100
'priority' => 100,
]);
Route::create([
'feed_id' => $feed2->id,
'platform_channel_id' => $channel->id,
'is_active' => false,
'priority' => 50
'priority' => 50,
]);
$feeds = $channel->feeds;
@ -168,8 +168,8 @@ public function test_belongs_to_many_feeds_relationship(): void
// Test pivot data
$feed1FromRelation = $feeds->find($feed1->id);
$this->assertEquals(1, $feed1FromRelation->pivot->is_active);
$this->assertEquals(100, $feed1FromRelation->pivot->priority);
$this->assertEquals(1, $feed1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $feed1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
}
public function test_active_feeds_relationship(): void
@ -184,21 +184,21 @@ public function test_active_feeds_relationship(): void
'feed_id' => $activeFeed1->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 100
'priority' => 100,
]);
Route::create([
'feed_id' => $activeFeed2->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 200
'priority' => 200,
]);
Route::create([
'feed_id' => $inactiveFeed->id,
'platform_channel_id' => $channel->id,
'is_active' => false,
'priority' => 150
'priority' => 150,
]);
$activeFeeds = $channel->activeFeeds;
@ -221,7 +221,7 @@ public function test_channel_creation_with_factory(): void
$this->assertInstanceOf(PlatformChannel::class, $channel);
$this->assertNotNull($channel->platform_instance_id);
$this->assertIsString($channel->name);
$this->assertIsString($channel->channel_id);
$this->assertIsString($channel->channel_id); // @phpstan-ignore method.impossibleType
$this->assertIsBool($channel->is_active);
}
@ -237,7 +237,7 @@ public function test_channel_creation_with_explicit_values(): void
'channel_id' => 'channel_123',
'description' => 'A test channel',
'language_id' => $language->id,
'is_active' => false
'is_active' => false,
]);
$this->assertEquals($instance->id, $channel->platform_instance_id);
@ -253,12 +253,12 @@ public function test_channel_update(): void
{
$channel = PlatformChannel::factory()->create([
'name' => 'original_name',
'is_active' => true
'is_active' => true,
]);
$channel->update([
'name' => 'updated_name',
'is_active' => false
'is_active' => false,
]);
$channel->refresh();
@ -281,7 +281,7 @@ public function test_channel_with_display_name(): void
{
$channel = PlatformChannel::factory()->create([
'name' => 'tech',
'display_name' => 'Technology Discussion'
'display_name' => 'Technology Discussion',
]);
$this->assertEquals('tech', $channel->name);
@ -292,7 +292,7 @@ public function test_channel_without_display_name(): void
{
$channel = PlatformChannel::factory()->create([
'name' => 'general',
'display_name' => 'General'
'display_name' => 'General',
]);
$this->assertEquals('general', $channel->name);
@ -320,7 +320,7 @@ public function test_channel_can_have_multiple_accounts_with_different_prioritie
$channel->platformAccounts()->attach([
$account1->id => ['is_active' => true, 'priority' => 300],
$account2->id => ['is_active' => true, 'priority' => 100],
$account3->id => ['is_active' => false, 'priority' => 200]
$account3->id => ['is_active' => false, 'priority' => 200],
]);
$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
foreach ($allAccounts as $account) {
$this->assertNotNull($account->pivot->priority);
$this->assertIsInt($account->pivot->is_active);
$this->assertNotNull($account->pivot->priority); // @phpstan-ignore property.notFound
$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
{
$fillableFields = ['platform', 'url', 'name', 'description', 'is_active'];
$instance = new PlatformInstance();
$instance = new PlatformInstance;
$this->assertEquals($fillableFields, $instance->getFillable());
}
public function test_table_name(): void
{
$instance = new PlatformInstance();
$instance = new PlatformInstance;
$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->refresh();
$this->assertIsBool($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
{
$instance = PlatformInstance::factory()->create();
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]);
$channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]);
// Create channel for different instance
$otherInstance = PlatformInstance::factory()->create();
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
$instance->languages()->attach($language1->id, [
'platform_language_id' => 1,
'is_default' => true
'is_default' => true,
]);
$instance->languages()->attach($language2->id, [
'platform_language_id' => 2,
'is_default' => false
'is_default' => false,
]);
$languages = $instance->languages;
@ -95,27 +95,27 @@ public function test_belongs_to_many_languages_relationship(): void
// Test pivot data
$language1FromRelation = $languages->find($language1->id);
$this->assertEquals(1, $language1FromRelation->pivot->platform_language_id);
$this->assertEquals(1, $language1FromRelation->pivot->is_default); // Database returns 1 for true
$this->assertEquals(1, $language1FromRelation->pivot->platform_language_id); // @phpstan-ignore property.notFound
$this->assertEquals(1, $language1FromRelation->pivot->is_default); // @phpstan-ignore property.notFound
$language2FromRelation = $languages->find($language2->id);
$this->assertEquals(2, $language2FromRelation->pivot->platform_language_id);
$this->assertEquals(0, $language2FromRelation->pivot->is_default); // Database returns 0 for false
$this->assertEquals(2, $language2FromRelation->pivot->platform_language_id); // @phpstan-ignore property.notFound
$this->assertEquals(0, $language2FromRelation->pivot->is_default); // @phpstan-ignore property.notFound
}
public function test_find_by_url_static_method(): void
{
$url = 'https://lemmy.world';
$instance1 = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'url' => $url
'url' => $url,
]);
// Create instance with different URL
PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.ml'
'url' => 'https://lemmy.ml',
]);
$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
{
$url = 'https://example.com';
// Create instance with same URL but different platform won't be found
PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'url' => $url
'url' => $url,
]);
// 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',
'name' => 'Lemmy World',
'description' => 'A general purpose Lemmy instance',
'is_active' => false
'is_active' => false,
]);
$this->assertEquals(PlatformEnum::LEMMY, $instance->platform);
@ -191,12 +191,12 @@ public function test_instance_update(): void
{
$instance = PlatformInstance::factory()->create([
'name' => 'Original Name',
'is_active' => true
'is_active' => true,
]);
$instance->update([
'name' => 'Updated Name',
'is_active' => false
'is_active' => false,
]);
$instance->refresh();
@ -219,7 +219,7 @@ public function test_instance_can_have_null_description(): void
{
$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
@ -265,7 +265,7 @@ public function test_instance_can_have_multiple_languages(): void
$instance->languages()->attach([
$language1->id => ['platform_language_id' => 1, 'is_default' => true],
$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;
@ -274,12 +274,12 @@ public function test_instance_can_have_multiple_languages(): void
// Test that we can access pivot data
foreach ($languages as $language) {
$this->assertNotNull($language->pivot->platform_language_id);
$this->assertContains($language->pivot->is_default, [0, 1, true, false]); // Can be int or bool
$this->assertNotNull($language->pivot->platform_language_id); // @phpstan-ignore property.notFound
$this->assertContains($language->pivot->is_default, [0, 1, true, false]); // @phpstan-ignore property.notFound
}
// 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);
}
@ -301,12 +301,12 @@ public function test_multiple_instances_with_same_platform(): void
{
$instance1 = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'name' => 'Lemmy World'
'name' => 'Lemmy World',
]);
$instance2 = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'name' => 'Lemmy ML'
'name' => 'Lemmy ML',
]);
$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(PlatformEnum::LEMMY, $instance->platform);
}
}
}

View file

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

View file

@ -12,7 +12,7 @@ class LemmyRequestTest extends TestCase
public function test_constructor_with_simple_domain(): void
{
$request = new LemmyRequest('lemmy.world');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
$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
{
$request = new LemmyRequest('https://lemmy.world');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
$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
{
$request = new LemmyRequest('http://lemmy.world');
$this->assertEquals('http', $this->getPrivateProperty($request, 'scheme'));
$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
{
$request = new LemmyRequest('lemmy.world/');
$this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance'));
}
public function test_constructor_with_full_url_and_trailing_slash(): void
{
$request = new LemmyRequest('https://lemmy.world/');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
$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
{
$request = new LemmyRequest('lemmy.world', 'test-token');
$this->assertEquals('test-token', $this->getPrivateProperty($request, 'token'));
}
public function test_constructor_preserves_case_in_scheme_detection(): void
{
$request = new LemmyRequest('HTTPS://lemmy.world');
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
}
@ -67,7 +67,7 @@ public function test_with_scheme_sets_https(): void
{
$request = new LemmyRequest('lemmy.world');
$result = $request->withScheme('https');
$this->assertSame($request, $result);
$this->assertEquals('https', $this->getPrivateProperty($request, 'scheme'));
}
@ -76,7 +76,7 @@ public function test_with_scheme_sets_http(): void
{
$request = new LemmyRequest('lemmy.world');
$result = $request->withScheme('http');
$this->assertSame($request, $result);
$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->withScheme('HTTPS');
$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');
$originalScheme = $this->getPrivateProperty($request, 'scheme');
$request->withScheme('ftp');
$this->assertEquals($originalScheme, $this->getPrivateProperty($request, 'scheme'));
}
@ -103,7 +103,7 @@ public function test_with_token_sets_token(): void
{
$request = new LemmyRequest('lemmy.world');
$result = $request->withToken('new-token');
$this->assertSame($request, $result);
$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
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$response = $request->get('site');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/site'
&& !$httpRequest->hasHeader('Authorization');
&& ! $httpRequest->hasHeader('Authorization');
});
}
public function test_get_with_token(): void
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world', 'test-token');
$response = $request->get('site');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/site'
&& $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
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$params = ['limit' => 10, 'page' => 1];
$response = $request->get('posts', $params);
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) use ($params) {
Http::assertSent(function ($httpRequest) {
$url = $httpRequest->url();
return str_contains($url, 'https://lemmy.world/api/v3/posts')
&& str_contains($url, 'limit=10')
&& str_contains($url, 'page=1');
@ -159,13 +160,13 @@ public function test_get_with_parameters(): void
public function test_get_with_http_scheme(): void
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$request->withScheme('http');
$response = $request->get('site');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
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
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$response = $request->post('login');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/login'
&& $httpRequest->method() === 'POST'
&& !$httpRequest->hasHeader('Authorization');
&& ! $httpRequest->hasHeader('Authorization');
});
}
public function test_post_with_token(): void
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world', 'test-token');
$response = $request->post('login');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/login'
&& $httpRequest->method() === 'POST'
@ -206,13 +207,13 @@ public function test_post_with_token(): void
public function test_post_with_data(): void
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$data = ['username' => 'test', 'password' => 'pass'];
$response = $request->post('login', $data);
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) use ($data) {
return $httpRequest->url() === 'https://lemmy.world/api/v3/login'
&& $httpRequest->method() === 'POST'
@ -223,13 +224,13 @@ public function test_post_with_data(): void
public function test_post_with_http_scheme(): void
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$request->withScheme('http');
$response = $request->post('login');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
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
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$request->get('site');
Http::assertSent(function ($httpRequest) {
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
{
Http::fake(['*' => Http::response(['success' => true])]);
$request = new LemmyRequest('lemmy.world');
$response = $request->withScheme('http')->withToken('chained-token')->get('site');
$this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) {
return $httpRequest->url() === 'http://lemmy.world/api/v3/site'
&& $httpRequest->header('Authorization')[0] === 'Bearer chained-token';
@ -267,7 +268,7 @@ private function getPrivateProperty(object $object, string $property): mixed
$reflection = new \ReflectionClass($object);
$reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($object);
}
}
}

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