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

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

View file

@ -23,6 +23,8 @@ public function sendResponse(mixed $result, string $message = 'Success', int $co
/** /**
* Error response method * Error response method
*
* @param array<string, mixed> $errorMessages
*/ */
public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse
{ {
@ -40,6 +42,8 @@ public function sendError(string $error, array $errorMessages = [], int $code =
/** /**
* Validation error response method * Validation error response method
*
* @param array<string, mixed> $errors
*/ */
public function sendValidationError(array $errors): JsonResponse public function sendValidationError(array $errors): JsonResponse
{ {

View file

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

View file

@ -84,8 +84,10 @@ public function options(): JsonResponse
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']); ->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
// Get feed providers from config // Get feed providers from config
$feedProviders = collect(config('feed.providers', [])) /** @var array<string, array<string, mixed>> $providers */
->filter(fn($provider) => $provider['is_active']) $providers = config('feed.providers', []);
$feedProviders = collect($providers)
->filter(fn (array $provider) => $provider['is_active'])
->values(); ->values();
return $this->sendResponse([ return $this->sendResponse([

View file

@ -5,8 +5,8 @@
use App\Actions\CreateChannelAction; use App\Actions\CreateChannelAction;
use App\Http\Requests\StorePlatformChannelRequest; use App\Http\Requests\StorePlatformChannelRequest;
use App\Http\Resources\PlatformChannelResource; use App\Http\Resources\PlatformChannelResource;
use App\Models\PlatformChannel;
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use Exception; use Exception;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -144,6 +144,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
'priority' => 'nullable|integer|min:1|max:100', 'priority' => 'nullable|integer|min:1|max:100',
]); ]);
/** @var PlatformAccount $platformAccount */
$platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']); $platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']);
// Check if account is already attached // Check if account is already attached

View file

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

View file

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

View file

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

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\Feed
*/
class FeedResource extends JsonResource class FeedResource extends JsonResource
{ {
/** /**

View file

@ -5,6 +5,12 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\PlatformAccount
*/
/**
* @mixin \App\Models\PlatformAccount
*/
class PlatformAccountResource extends JsonResource class PlatformAccountResource extends JsonResource
{ {
/** /**

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\PlatformChannel
*/
class PlatformChannelResource extends JsonResource class PlatformChannelResource extends JsonResource
{ {
/** /**

View file

@ -5,6 +5,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Models\PlatformInstance
*/
class PlatformInstanceResource extends JsonResource class PlatformInstanceResource extends JsonResource
{ {
/** /**

View file

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

View file

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

View file

@ -51,7 +51,7 @@ public function detachAccount(int $channelId, int $accountId): void
$channel->platformAccounts()->detach($accountId); $channel->platformAccounts()->detach($accountId);
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get(); $channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get();
$allAccounts = PlatformAccount::where('is_active', true)->get(); $allAccounts = PlatformAccount::where('is_active', true)->get();

View file

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

View file

@ -14,7 +14,7 @@ public function toggle(int $feedId): void
$feed->save(); $feed->save();
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$feeds = Feed::orderBy('name')->get(); $feeds = Feed::orderBy('name')->get();

View file

@ -29,36 +29,54 @@ class Onboarding extends Component
// Platform form // Platform form
public string $instanceUrl = ''; public string $instanceUrl = '';
public string $username = ''; public string $username = '';
public string $password = ''; public string $password = '';
/** @var array<string, mixed>|null */
public ?array $existingAccount = null; public ?array $existingAccount = null;
// Feed form // Feed form
public string $feedName = ''; public string $feedName = '';
public string $feedProvider = 'vrt'; public string $feedProvider = 'vrt';
public ?int $feedLanguageId = null; public ?int $feedLanguageId = null;
public string $feedDescription = ''; public string $feedDescription = '';
// Channel form // Channel form
public string $channelName = ''; public string $channelName = '';
public ?int $platformInstanceId = null; public ?int $platformInstanceId = null;
public ?int $channelLanguageId = null; public ?int $channelLanguageId = null;
public string $channelDescription = ''; public string $channelDescription = '';
// Route form // Route form
public ?int $routeFeedId = null; public ?int $routeFeedId = null;
public ?int $routeChannelId = null; public ?int $routeChannelId = null;
public int $routePriority = 50; public int $routePriority = 50;
// State // State
/** @var array<string, string> */
public array $formErrors = []; public array $formErrors = [];
public bool $isLoading = false; public bool $isLoading = false;
#[\Livewire\Attributes\Locked] #[\Livewire\Attributes\Locked]
public ?int $previousChannelLanguageId = null; public ?int $previousChannelLanguageId = null;
protected CreatePlatformAccountAction $createPlatformAccountAction; protected CreatePlatformAccountAction $createPlatformAccountAction;
protected CreateFeedAction $createFeedAction; protected CreateFeedAction $createFeedAction;
protected CreateChannelAction $createChannelAction; protected CreateChannelAction $createChannelAction;
protected CreateRouteAction $createRouteAction; protected CreateRouteAction $createRouteAction;
public function boot( public function boot(
@ -319,6 +337,9 @@ public function completeOnboarding(): void
/** /**
* Get language codes that have at least one active provider. * Get language codes that have at least one active provider.
*/ */
/**
* @return list<string>
*/
public function getAvailableLanguageCodes(): array public function getAvailableLanguageCodes(): array
{ {
$providers = config('feed.providers', []); $providers = config('feed.providers', []);
@ -329,7 +350,7 @@ public function getAvailableLanguageCodes(): array
continue; continue;
} }
foreach (array_keys($provider['languages'] ?? []) as $code) { foreach (array_keys($provider['languages'] ?? []) as $code) {
$languageCodes[$code] = true; $languageCodes[(string) $code] = true;
} }
} }
@ -339,6 +360,9 @@ public function getAvailableLanguageCodes(): array
/** /**
* Get providers available for the current channel language. * Get providers available for the current channel language.
*/ */
/**
* @return array<int, array<string, string>>
*/
public function getProvidersForLanguage(): array public function getProvidersForLanguage(): array
{ {
if (! $this->channelLanguageId) { if (! $this->channelLanguageId) {
@ -378,10 +402,11 @@ public function getChannelLanguage(): ?Language
if (! $this->channelLanguageId) { if (! $this->channelLanguageId) {
return null; return null;
} }
return Language::find($this->channelLanguageId); return Language::find($this->channelLanguageId);
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
// For channel step: only show languages that have providers // For channel step: only show languages that have providers
$availableCodes = $this->getAvailableLanguageCodes(); $availableCodes = $this->getAvailableLanguageCodes();

View file

@ -11,12 +11,16 @@
class Routes extends Component class Routes extends Component
{ {
public bool $showCreateModal = false; public bool $showCreateModal = false;
public ?int $editingFeedId = null; public ?int $editingFeedId = null;
public ?int $editingChannelId = null; public ?int $editingChannelId = null;
// Create form // Create form
public ?int $newFeedId = null; public ?int $newFeedId = null;
public ?int $newChannelId = null; public ?int $newChannelId = null;
public int $newPriority = 50; public int $newPriority = 50;
// Edit form // Edit form
@ -24,6 +28,7 @@ class Routes extends Component
// Keyword management // Keyword management
public string $newKeyword = ''; public string $newKeyword = '';
public bool $showKeywordInput = false; public bool $showKeywordInput = false;
public function openCreateModal(): void public function openCreateModal(): void
@ -53,6 +58,7 @@ public function createRoute(): void
if ($exists) { if ($exists) {
$this->addError('newFeedId', 'This route already exists.'); $this->addError('newFeedId', 'This route already exists.');
return; return;
} }
@ -153,7 +159,7 @@ public function deleteKeyword(int $keywordId): void
Keyword::destroy($keywordId); Keyword::destroy($keywordId);
} }
public function render() public function render(): \Illuminate\Contracts\View\View
{ {
$routes = Route::with(['feed', 'platformChannel']) $routes = Route::with(['feed', 'platformChannel'])
->orderBy('priority', 'desc') ->orderBy('priority', 'desc')
@ -168,7 +174,8 @@ public function render()
$routes = $routes->map(function ($route) use ($allKeywords) { $routes = $routes->map(function ($route) use ($allKeywords) {
$key = $route->feed_id.'-'.$route->platform_channel_id; $key = $route->feed_id.'-'.$route->platform_channel_id;
$route->keywords = $allKeywords->get($key, collect()); $route->setRelation('keywords', $allKeywords->get($key, collect()));
return $route; return $route;
}); });

View file

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

View file

@ -15,15 +15,20 @@
* @method static firstOrCreate(array<string, mixed> $array) * @method static firstOrCreate(array<string, mixed> $array)
* @method static where(string $string, string $url) * @method static where(string $string, string $url)
* @method static create(array<string, mixed> $array) * @method static create(array<string, mixed> $array)
* @property integer $id *
* @property int $id
* @property int $feed_id * @property int $feed_id
* @property Feed $feed * @property Feed $feed
* @property string $url * @property string $url
* @property string $title
* @property string|null $description
* @property string $approval_status
* @property string $publish_status
* @property bool|null $is_valid * @property bool|null $is_valid
* @property Carbon|null $validated_at * @property Carbon|null $validated_at
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at * @property Carbon $updated_at
* @property ArticlePublication $articlePublication * @property ArticlePublication|null $articlePublication
*/ */
class Article extends Model class Article extends Model
{ {
@ -79,7 +84,7 @@ public function isRejected(): bool
return $this->approval_status === 'rejected'; return $this->approval_status === 'rejected';
} }
public function approve(string $approvedBy = null): void public function approve(?string $approvedBy = null): void
{ {
$this->update([ $this->update([
'approval_status' => 'approved', 'approval_status' => 'approved',
@ -89,7 +94,7 @@ public function approve(string $approvedBy = null): void
event(new ArticleApproved($this)); event(new ArticleApproved($this));
} }
public function reject(string $rejectedBy = null): void public function reject(?string $rejectedBy = null): void
{ {
$this->update([ $this->update([
'approval_status' => 'rejected', 'approval_status' => 'rejected',

View file

@ -8,9 +8,16 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property integer $article_id * @property int $id
* @property integer $platform_channel_id * @property int $article_id
* @property integer $post_id * @property int $platform_channel_id
* @property string $post_id
* @property string $platform
* @property string $published_by
* @property array<string, mixed>|null $publication_data
* @property \Illuminate\Support\Carbon $published_at
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* *
* @method static create(array<string, mixed> $array) * @method static create(array<string, mixed> $array)
*/ */

View file

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

View file

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

View file

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

View file

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

View file

@ -2,15 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Enums\PlatformEnum;
use Database\Factories\PlatformAccountFactory; use Database\Factories\PlatformAccountFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use App\Enums\PlatformEnum;
/** /**
* @property int $id * @property int $id
@ -18,13 +18,14 @@
* @property string $instance_url * @property string $instance_url
* @property string $username * @property string $username
* @property string $password * @property string $password
* @property string $settings * @property array<string, mixed> $settings
* @property bool $is_active * @property bool $is_active
* @property Carbon $last_tested_at * @property Carbon|null $last_tested_at
* @property string $status * @property string $status
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at * @property Carbon $updated_at
* @property Collection<int, PlatformChannel> $activeChannels * @property Collection<int, PlatformChannel> $activeChannels
*
* @method static where(string $string, PlatformEnum $platform) * @method static where(string $string, PlatformEnum $platform)
* @method static orderBy(string $string) * @method static orderBy(string $string)
* @method static create(array<string, mixed> $validated) * @method static create(array<string, mixed> $validated)
@ -42,14 +43,14 @@ class PlatformAccount extends Model
'settings', 'settings',
'is_active', 'is_active',
'last_tested_at', 'last_tested_at',
'status' 'status',
]; ];
protected $casts = [ protected $casts = [
'platform' => PlatformEnum::class, 'platform' => PlatformEnum::class,
'settings' => 'array', 'settings' => 'array',
'is_active' => 'boolean', 'is_active' => 'boolean',
'last_tested_at' => 'datetime' 'last_tested_at' => 'datetime',
]; ];
// Encrypt password when storing // Encrypt password when storing
@ -93,7 +94,6 @@ protected function password(): Attribute
)->withoutObjectCaching(); )->withoutObjectCaching();
} }
// Get the active accounts for a platform (returns collection) // Get the active accounts for a platform (returns collection)
/** /**
* @return Collection<int, PlatformAccount> * @return Collection<int, PlatformAccount>

View file

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

View file

@ -12,7 +12,9 @@
*/ */
class PlatformChannelPost extends Model class PlatformChannelPost extends Model
{ {
/** @use HasFactory<\Illuminate\Database\Eloquent\Factories\Factory<PlatformChannelPost>> */
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'platform', 'platform',
'channel_id', 'channel_id',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,6 +61,7 @@ public function login(string $username, string $password): ?string
} }
$data = $response->json(); $data = $response->json();
return $data['jwt'] ?? null; return $data['jwt'] ?? null;
} catch (Exception $e) { } catch (Exception $e) {
// Re-throw rate limit exceptions immediately // Re-throw rate limit exceptions immediately
@ -93,6 +94,7 @@ public function getCommunityId(string $communityName, string $token): int
} }
$data = $response->json(); $data = $response->json();
return $data['community_view']['community']['id'] ?? throw new Exception('Community not found'); return $data['community_view']['community']['id'] ?? throw new Exception('Community not found');
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Community lookup failed', ['error' => $e->getMessage()]); logger()->error('Community lookup failed', ['error' => $e->getMessage()]);
@ -107,14 +109,15 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
$response = $request->get('post/list', [ $response = $request->get('post/list', [
'community_id' => $platformChannelId, 'community_id' => $platformChannelId,
'limit' => 50, 'limit' => 50,
'sort' => 'New' 'sort' => 'New',
]); ]);
if (! $response->successful()) { if (! $response->successful()) {
logger()->warning('Failed to sync channel posts', [ logger()->warning('Failed to sync channel posts', [
'status' => $response->status(), 'status' => $response->status(),
'platform_channel_id' => $platformChannelId 'platform_channel_id' => $platformChannelId,
]); ]);
return; return;
} }
@ -137,13 +140,13 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
logger()->info('Synced channel posts', [ logger()->info('Synced channel posts', [
'platform_channel_id' => $platformChannelId, 'platform_channel_id' => $platformChannelId,
'posts_count' => count($posts) 'posts_count' => count($posts),
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Exception while syncing channel posts', [ logger()->error('Exception while syncing channel posts', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'platform_channel_id' => $platformChannelId 'platform_channel_id' => $platformChannelId,
]); ]);
} }
} }
@ -198,17 +201,20 @@ public function getLanguages(): array
if (! $response->successful()) { if (! $response->successful()) {
logger()->warning('Failed to fetch site languages', [ logger()->warning('Failed to fetch site languages', [
'status' => $response->status() 'status' => $response->status(),
]); ]);
return []; return [];
} }
$data = $response->json(); $data = $response->json();
return $data['all_languages'] ?? []; return $data['all_languages'] ?? [];
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Exception while fetching languages', [ logger()->error('Exception while fetching languages', [
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
return []; return [];
} }
} }

View file

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

View file

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

View file

@ -28,7 +28,7 @@ public function validate(Article $article): Article
if (! isset($articleData['full_article']) || empty($articleData['full_article'])) { if (! isset($articleData['full_article']) || empty($articleData['full_article'])) {
logger()->warning('Article data missing full_article content', [ logger()->warning('Article data missing full_article content', [
'article_id' => $article->id, 'article_id' => $article->id,
'url' => $article->url 'url' => $article->url,
]); ]);
$updateData['approval_status'] = 'rejected'; $updateData['approval_status'] = 'rejected';
@ -67,7 +67,7 @@ private function validateByKeywords(string $full_article): bool
// Common Belgian news topics // Common Belgian news topics
'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy', 'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy',
'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police' 'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police',
]; ];
foreach ($keywords as $keyword) { foreach ($keywords as $keyword) {

View file

@ -52,8 +52,12 @@ public function refreshToken(PlatformAccount $account): string
/** /**
* Authenticate with Lemmy API and return user data with JWT * Authenticate with Lemmy API and return user data with JWT
*
* @throws PlatformAuthException * @throws PlatformAuthException
*/ */
/**
* @return array<string, mixed>
*/
public function authenticate(string $instanceUrl, string $username, string $password): array public function authenticate(string $instanceUrl, string $username, string $password): array
{ {
try { try {
@ -75,8 +79,8 @@ public function authenticate(string $instanceUrl, string $username, string $pass
'id' => 0, // Would need API call to get actual user info 'id' => 0, // Would need API call to get actual user info
'display_name' => null, 'display_name' => null,
'bio' => null, 'bio' => null,
] ],
] ],
]; ];
} catch (PlatformAuthException $e) { } catch (PlatformAuthException $e) {
// Re-throw PlatformAuthExceptions as-is to avoid nesting // Re-throw PlatformAuthExceptions as-is to avoid nesting

View file

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

View file

@ -4,9 +4,9 @@
use App\Contracts\ArticleParserInterface; use App\Contracts\ArticleParserInterface;
use App\Models\Feed; use App\Models\Feed;
use App\Services\Parsers\VrtArticleParser;
use App\Services\Parsers\BelgaArticleParser; use App\Services\Parsers\BelgaArticleParser;
use App\Services\Parsers\GuardianArticleParser; use App\Services\Parsers\GuardianArticleParser;
use App\Services\Parsers\VrtArticleParser;
use Exception; use Exception;
class ArticleParserFactory class ArticleParserFactory
@ -26,7 +26,7 @@ class ArticleParserFactory
public static function getParser(string $url): ArticleParserInterface public static function getParser(string $url): ArticleParserInterface
{ {
foreach (self::$parsers as $parserClass) { foreach (self::$parsers as $parserClass) {
$parser = new $parserClass(); $parser = new $parserClass;
if ($parser->canParse($url)) { if ($parser->canParse($url)) {
return $parser; return $parser;
@ -47,12 +47,13 @@ public static function getParserForFeed(Feed $feed, string $parserType = 'articl
return null; return null;
} }
/** @var class-string<ArticleParserInterface> $parserClass */
$parserClass = $providerConfig['parsers'][$parserType]; $parserClass = $providerConfig['parsers'][$parserType];
if (! class_exists($parserClass)) { if (! class_exists($parserClass)) {
return null; return null;
} }
return new $parserClass(); return new $parserClass;
} }
/** /**
@ -61,7 +62,8 @@ public static function getParserForFeed(Feed $feed, string $parserType = 'articl
public static function getSupportedSources(): array public static function getSupportedSources(): array
{ {
return array_map(function ($parserClass) { return array_map(function ($parserClass) {
$parser = new $parserClass(); $parser = new $parserClass;
return $parser->getSourceName(); return $parser->getSourceName();
}, self::$parsers); }, self::$parsers);
} }

View file

@ -18,12 +18,13 @@ public static function getParserForFeed(Feed $feed): ?HomepageParserInterface
return null; return null;
} }
/** @var class-string<HomepageParserInterface> $parserClass */
$parserClass = $providerConfig['parsers']['homepage']; $parserClass = $providerConfig['parsers']['homepage'];
if (! class_exists($parserClass)) { if (! class_exists($parserClass)) {
return null; return null;
} }
$language = $feed->language?->short_code ?? 'en'; $language = $feed->language->short_code ?? 'en';
return new $parserClass($language); return new $parserClass($language);
} }

View file

@ -2,8 +2,8 @@
namespace App\Services\Http; namespace App\Services\Http;
use Illuminate\Support\Facades\Http;
use Exception; use Exception;
use Illuminate\Support\Facades\Http;
class HttpFetcher class HttpFetcher
{ {
@ -23,7 +23,7 @@ public static function fetchHtml(string $url): string
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('HTTP fetch failed', [ logger()->error('HTTP fetch failed', [
'url' => $url, 'url' => $url,
'error' => $e->getMessage() 'error' => $e->getMessage(),
]); ]);
throw $e; throw $e;
@ -54,14 +54,14 @@ public static function fetchMultipleUrls(array $urls): array
return [ return [
'url' => $url, 'url' => $url,
'html' => $response->body(), 'html' => $response->body(),
'success' => true 'success' => true,
]; ];
} else { } else {
return [ return [
'url' => $url, 'url' => $url,
'html' => null, 'html' => null,
'success' => false, 'success' => false,
'status' => $response->status() 'status' => $response->status(),
]; ];
} }
} catch (Exception) { } catch (Exception) {
@ -69,11 +69,10 @@ public static function fetchMultipleUrls(array $urls): array
'url' => $url, 'url' => $url,
'html' => null, 'html' => null,
'success' => false, 'success' => false,
'error' => 'Exception occurred' 'error' => 'Exception occurred',
]; ];
} }
}) })
->filter(fn($result) => $result !== null)
->toArray(); ->toArray();
} catch (Exception $e) { } catch (Exception $e) {
logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]); logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]);

View file

@ -34,6 +34,7 @@ public static function extractArticleUrls(string $html): array
return false; return false;
} }
} }
return true; return true;
}) })
->map(function ($path) { ->map(function ($path) {

View file

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

View file

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

View file

@ -11,6 +11,7 @@ class RoutingValidationService
{ {
/** /**
* @param Collection<int, PlatformChannel> $channels * @param Collection<int, PlatformChannel> $channels
*
* @throws RoutingMismatchException * @throws RoutingMismatchException
*/ */
public function validateLanguageCompatibility(Feed $feed, Collection $channels): void public function validateLanguageCompatibility(Feed $feed, Collection $channels): void

View file

@ -3,8 +3,8 @@
namespace App\Services; namespace App\Services;
use App\Models\Feed; use App\Models\Feed;
use App\Models\Route;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\Route;
use App\Models\Setting; use App\Models\Setting;
class SystemStatusService class SystemStatusService

View file

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

View file

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

View file

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

View file

@ -27,12 +27,12 @@ public function test_user_model_creates_successfully(): void
{ {
$user = User::factory()->create([ $user = User::factory()->create([
'name' => 'Test User', 'name' => 'Test User',
'email' => 'test@example.com' 'email' => 'test@example.com',
]); ]);
$this->assertDatabaseHas('users', [ $this->assertDatabaseHas('users', [
'name' => 'Test User', 'name' => 'Test User',
'email' => 'test@example.com' 'email' => 'test@example.com',
]); ]);
$this->assertEquals('Test User', $user->name); $this->assertEquals('Test User', $user->name);
@ -43,38 +43,40 @@ public function test_language_model_creates_successfully(): void
{ {
$language = Language::factory()->create([ $language = Language::factory()->create([
'name' => 'English', 'name' => 'English',
'short_code' => 'en' 'short_code' => 'en',
]); ]);
$this->assertDatabaseHas('languages', [ $this->assertDatabaseHas('languages', [
'name' => 'English', 'name' => 'English',
'short_code' => 'en' 'short_code' => 'en',
]); ]);
} }
public function test_platform_instance_model_creates_successfully(): void public function test_platform_instance_model_creates_successfully(): void
{ {
/** @var PlatformInstance $instance */
$instance = PlatformInstance::factory()->create([ $instance = PlatformInstance::factory()->create([
'name' => 'Test Instance', 'name' => 'Test Instance',
'url' => 'https://test.lemmy.world' 'url' => 'https://test.lemmy.world',
]); ]);
$this->assertDatabaseHas('platform_instances', [ $this->assertDatabaseHas('platform_instances', [
'name' => 'Test Instance', 'name' => 'Test Instance',
'url' => 'https://test.lemmy.world' 'url' => 'https://test.lemmy.world',
]); ]);
} }
public function test_platform_account_model_creates_successfully(): void public function test_platform_account_model_creates_successfully(): void
{ {
/** @var PlatformAccount $account */
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'username' => 'testuser', 'username' => 'testuser',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('platform_accounts', [ $this->assertDatabaseHas('platform_accounts', [
'username' => 'testuser', 'username' => 'testuser',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals('testuser', $account->username); $this->assertEquals('testuser', $account->username);
@ -89,14 +91,14 @@ public function test_platform_channel_model_creates_successfully(): void
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Channel', 'name' => 'Test Channel',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('platform_channels', [ $this->assertDatabaseHas('platform_channels', [
'platform_instance_id' => $instance->id, 'platform_instance_id' => $instance->id,
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Channel', 'name' => 'Test Channel',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($instance->id, $channel->platformInstance->id); $this->assertEquals($instance->id, $channel->platformInstance->id);
@ -111,14 +113,14 @@ public function test_feed_model_creates_successfully(): void
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed.rss', 'url' => 'https://example.com/feed.rss',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
'language_id' => $language->id, 'language_id' => $language->id,
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed.rss', 'url' => 'https://example.com/feed.rss',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($language->id, $feed->language->id); $this->assertEquals($language->id, $feed->language->id);
@ -132,14 +134,14 @@ public function test_article_model_creates_successfully(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'title' => 'Test Article', 'title' => 'Test Article',
'url' => 'https://example.com/article', 'url' => 'https://example.com/article',
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
$this->assertDatabaseHas('articles', [ $this->assertDatabaseHas('articles', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'title' => 'Test Article', 'title' => 'Test Article',
'url' => 'https://example.com/article', 'url' => 'https://example.com/article',
'approval_status' => 'pending' 'approval_status' => 'pending',
]); ]);
$this->assertEquals($feed->id, $article->feed->id); $this->assertEquals($feed->id, $article->feed->id);
@ -155,14 +157,14 @@ public function test_article_publication_model_creates_successfully(): void
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'post_id' => 'test-post-123', 'post_id' => 'test-post-123',
'published_at' => now(), 'published_at' => now(),
'published_by' => 'test-user' 'published_by' => 'test-user',
]); ]);
$this->assertDatabaseHas('article_publications', [ $this->assertDatabaseHas('article_publications', [
'article_id' => $article->id, 'article_id' => $article->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'post_id' => 'test-post-123', 'post_id' => 'test-post-123',
'published_by' => 'test-user' 'published_by' => 'test-user',
]); ]);
$this->assertEquals($article->id, $publication->article->id); $this->assertEquals($article->id, $publication->article->id);
@ -174,16 +176,17 @@ public function test_route_model_creates_successfully(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(); $channel = PlatformChannel::factory()->create();
/** @var Route $route */
$route = Route::factory()->create([ $route = Route::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('routes', [ $this->assertDatabaseHas('routes', [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
$this->assertEquals($feed->id, $route->feed->id); $this->assertEquals($feed->id, $route->feed->id);
@ -228,14 +231,14 @@ public function test_keyword_model_creates_successfully(): void
->forChannel($channel) ->forChannel($channel)
->create([ ->create([
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true 'is_active' => true,
]); ]);
$this->assertDatabaseHas('keywords', [ $this->assertDatabaseHas('keywords', [
'keyword' => 'test keyword', 'keyword' => 'test keyword',
'is_active' => true, 'is_active' => true,
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id 'platform_channel_id' => $channel->id,
]); ]);
} }
@ -245,12 +248,12 @@ public function test_log_model_creates_successfully(): void
'level' => 'info', 'level' => 'info',
'message' => 'Test log message', 'message' => 'Test log message',
'context' => json_encode(['key' => 'value']), 'context' => json_encode(['key' => 'value']),
'logged_at' => now() 'logged_at' => now(),
]); ]);
$this->assertDatabaseHas('logs', [ $this->assertDatabaseHas('logs', [
'level' => 'info', 'level' => 'info',
'message' => 'Test log message' 'message' => 'Test log message',
]); ]);
} }
@ -258,12 +261,12 @@ public function test_setting_model_creates_successfully(): void
{ {
$setting = Setting::create([ $setting = Setting::create([
'key' => 'test_setting', 'key' => 'test_setting',
'value' => 'test_value' 'value' => 'test_value',
]); ]);
$this->assertDatabaseHas('settings', [ $this->assertDatabaseHas('settings', [
'key' => 'test_setting', 'key' => 'test_setting',
'value' => 'test_value' 'value' => 'test_value',
]); ]);
} }
@ -290,7 +293,7 @@ public function test_platform_account_channels_many_to_many_relationship(): void
$this->assertDatabaseHas('platform_account_channels', [ $this->assertDatabaseHas('platform_account_channels', [
'platform_account_id' => $account->id, 'platform_account_id' => $account->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true 'is_active' => true,
]); ]);
} }
@ -324,7 +327,7 @@ public function test_model_soft_deletes_work_correctly(): void
// Should find with withTrashed if model uses soft deletes // Should find with withTrashed if model uses soft deletes
if (method_exists($feed, 'withTrashed')) { if (method_exists($feed, 'withTrashed')) {
$this->assertNotNull(Feed::withTrashed()->find($feedId)); $this->assertNotNull(Feed::withTrashed()->find($feedId)); // @phpstan-ignore staticMethod.notFound
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -20,9 +20,9 @@ public function test_index_returns_successful_response(): void
'success', 'success',
'data' => [ 'data' => [
'feeds', 'feeds',
'pagination' 'pagination',
], ],
'message' 'message',
]); ]);
} }
@ -69,7 +69,7 @@ public function test_store_creates_vrt_feed_successfully(): void
'url' => 'https://www.vrt.be/vrtnws/en/', 'url' => 'https://www.vrt.be/vrtnws/en/',
'type' => 'website', 'type' => 'website',
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -101,7 +101,7 @@ public function test_store_creates_belga_feed_successfully(): void
'url' => 'https://www.belganewsagency.eu/', 'url' => 'https://www.belganewsagency.eu/',
'type' => 'website', 'type' => 'website',
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -133,7 +133,7 @@ public function test_store_creates_guardian_feed_successfully(): void
'url' => 'https://www.theguardian.com/international/rss', 'url' => 'https://www.theguardian.com/international/rss',
'type' => 'rss', 'type' => 'rss',
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -160,7 +160,7 @@ public function test_store_sets_default_active_status(): void
->assertJson([ ->assertJson([
'data' => [ 'data' => [
'is_active' => true, // Should default to true 'is_active' => true, // Should default to true
] ],
]); ]);
} }
@ -201,7 +201,7 @@ public function test_show_returns_feed_successfully(): void
'data' => [ 'data' => [
'id' => $feed->id, 'id' => $feed->id,
'name' => $feed->name, 'name' => $feed->name,
] ],
]); ]);
} }
@ -232,7 +232,7 @@ public function test_update_modifies_feed_successfully(): void
'message' => 'Feed updated successfully!', 'message' => 'Feed updated successfully!',
'data' => [ 'data' => [
'name' => 'Updated Name', 'name' => 'Updated Name',
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -260,7 +260,7 @@ public function test_update_preserves_active_status_when_not_provided(): void
->assertJson([ ->assertJson([
'data' => [ 'data' => [
'is_active' => false, // Should preserve original value 'is_active' => false, // Should preserve original value
] ],
]); ]);
} }
@ -298,7 +298,7 @@ public function test_toggle_activates_inactive_feed(): void
'message' => 'Feed activated successfully!', 'message' => 'Feed activated successfully!',
'data' => [ 'data' => [
'is_active' => true, 'is_active' => true,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [
@ -319,7 +319,7 @@ public function test_toggle_deactivates_active_feed(): void
'message' => 'Feed deactivated successfully!', 'message' => 'Feed deactivated successfully!',
'data' => [ 'data' => [
'is_active' => false, 'is_active' => false,
] ],
]); ]);
$this->assertDatabaseHas('feeds', [ $this->assertDatabaseHas('feeds', [

View file

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

View file

@ -37,7 +37,7 @@ public function test_index_returns_successful_response(): void
'context', 'context',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ],
], ],
'pagination' => [ 'pagination' => [
'current_page', 'current_page',
@ -46,12 +46,12 @@ public function test_index_returns_successful_response(): void
'total', 'total',
'from', 'from',
'to', 'to',
] ],
] ],
]) ])
->assertJson([ ->assertJson([
'success' => true, 'success' => true,
'message' => 'Logs retrieved successfully.' 'message' => 'Logs retrieved successfully.',
]); ]);
} }
@ -147,8 +147,8 @@ public function test_index_handles_empty_logs(): void
'total' => 0, 'total' => 0,
'current_page' => 1, 'current_page' => 1,
'last_page' => 1, 'last_page' => 1,
] ],
] ],
]); ]);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,8 +3,8 @@
namespace Tests; namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\Http;
use Mockery; use Mockery;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase

View file

@ -21,6 +21,7 @@ protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetc
return new ArticleFetcher($logSaver); return new ArticleFetcher($logSaver);
} }
/** @return array{ArticleFetcher, \Mockery\MockInterface} */
protected function createArticleFetcherWithMockedLogSaver(): array protected function createArticleFetcherWithMockedLogSaver(): array
{ {
$logSaver = Mockery::mock(LogSaver::class); $logSaver = Mockery::mock(LogSaver::class);

View file

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

View file

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

View file

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

View file

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

View file

@ -83,8 +83,8 @@ public function test_enum_can_be_compared(): void
$debug2 = LogLevelEnum::DEBUG; $debug2 = LogLevelEnum::DEBUG;
$info = LogLevelEnum::INFO; $info = LogLevelEnum::INFO;
$this->assertTrue($debug1 === $debug2); $this->assertTrue($debug1 === $debug2); // @phpstan-ignore identical.alwaysTrue
$this->assertFalse($debug1 === $info); $this->assertFalse($debug1 === $info); // @phpstan-ignore identical.alwaysFalse, method.impossibleType
} }
public function test_enum_can_be_used_in_match_expression(): void public function test_enum_can_be_used_in_match_expression(): void

View file

@ -54,7 +54,7 @@ public function test_enum_can_be_compared(): void
$lemmy1 = PlatformEnum::LEMMY; $lemmy1 = PlatformEnum::LEMMY;
$lemmy2 = PlatformEnum::LEMMY; $lemmy2 = PlatformEnum::LEMMY;
$this->assertTrue($lemmy1 === $lemmy2); $this->assertTrue($lemmy1 === $lemmy2); // @phpstan-ignore identical.alwaysTrue
} }
public function test_enum_can_be_used_in_match_expression(): void public function test_enum_can_be_used_in_match_expression(): void

View file

@ -3,11 +3,11 @@
namespace Tests\Unit\Facades; namespace Tests\Unit\Facades;
use App\Enums\LogLevelEnum; use App\Enums\LogLevelEnum;
use App\Enums\PlatformEnum;
use App\Facades\LogSaver; use App\Facades\LogSaver;
use App\Models\Log; use App\Models\Log;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\PlatformInstance; use App\Models\PlatformInstance;
use App\Enums\PlatformEnum;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -92,12 +92,12 @@ public function test_facade_works_with_channel(): void
{ {
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://facade.test.com' 'url' => 'https://facade.test.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'Facade Test Channel', 'name' => 'Facade Test Channel',
'platform_instance_id' => $platformInstance->id 'platform_instance_id' => $platformInstance->id,
]); ]);
$message = 'Facade channel test'; $message = 'Facade channel test';

View file

@ -54,7 +54,7 @@ public function test_handle_fetches_articles_and_updates_feed(): void
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed', 'url' => 'https://example.com/feed',
'last_fetched_at' => null 'last_fetched_at' => null,
]); ]);
$mockArticles = collect(['article1', 'article2']); $mockArticles = collect(['article1', 'article2']);
@ -72,7 +72,7 @@ public function test_handle_fetches_articles_and_updates_feed(): void
->with('Starting feed article fetch', null, [ ->with('Starting feed article fetch', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_name' => $feed->name, 'feed_name' => $feed->name,
'feed_url' => $feed->url 'feed_url' => $feed->url,
]) ])
->once(); ->once();
@ -80,7 +80,7 @@ public function test_handle_fetches_articles_and_updates_feed(): void
->with('Feed article fetch completed', null, [ ->with('Feed article fetch completed', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_name' => $feed->name, 'feed_name' => $feed->name,
'articles_count' => 2 'articles_count' => 2,
]) ])
->once(); ->once();
@ -180,7 +180,7 @@ public function test_handle_logs_start_message_with_correct_context(): void
// Arrange // Arrange
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'name' => 'Test Feed', 'name' => 'Test Feed',
'url' => 'https://example.com/feed' 'url' => 'https://example.com/feed',
]); ]);
$mockArticles = collect([]); $mockArticles = collect([]);
@ -197,7 +197,7 @@ public function test_handle_logs_start_message_with_correct_context(): void
->with('Starting feed article fetch', null, [ ->with('Starting feed article fetch', null, [
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'feed_name' => 'Test Feed', 'feed_name' => 'Test Feed',
'feed_url' => 'https://example.com/feed' 'feed_url' => 'https://example.com/feed',
]) ])
->once(); ->once();

View file

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

View file

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

View file

@ -3,12 +3,10 @@
namespace Tests\Unit\Jobs; namespace Tests\Unit\Jobs;
use App\Enums\PlatformEnum; use App\Enums\PlatformEnum;
use App\Exceptions\PlatformAuthException;
use App\Jobs\SyncChannelPostsJob; use App\Jobs\SyncChannelPostsJob;
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Models\PlatformInstance; use App\Models\PlatformInstance;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Services\Log\LogSaver; use App\Services\Log\LogSaver;
use Exception; use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -67,24 +65,24 @@ public function test_dispatch_for_all_active_channels_dispatches_jobs(): void
{ {
// Arrange // Arrange
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY 'platform' => PlatformEnum::LEMMY,
]); ]);
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'instance_url' => $platformInstance->url, 'instance_url' => $platformInstance->url,
'is_active' => true 'is_active' => true,
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id, 'platform_instance_id' => $platformInstance->id,
'is_active' => true 'is_active' => true,
]); ]);
// Attach account to channel with active status // Attach account to channel with active status
$channel->platformAccounts()->attach($account->id, [ $channel->platformAccounts()->attach($account->id, [
'is_active' => true, 'is_active' => true,
'created_at' => now(), 'created_at' => now(),
'updated_at' => now() 'updated_at' => now(),
]); ]);
// Mock LogSaver to avoid strict expectations // Mock LogSaver to avoid strict expectations
@ -105,12 +103,12 @@ public function test_handle_logs_start_message(): void
// Arrange // Arrange
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.example.com' 'url' => 'https://lemmy.example.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id, 'platform_instance_id' => $platformInstance->id,
'name' => 'testcommunity' 'name' => 'testcommunity',
]); ]);
// Mock LogSaver - only test that logging methods are called // Mock LogSaver - only test that logging methods are called
@ -136,7 +134,7 @@ public function test_job_can_be_serialized(): void
$platformInstance = PlatformInstance::factory()->create(); $platformInstance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id, 'platform_instance_id' => $platformInstance->id,
'name' => 'Test Channel' 'name' => 'Test Channel',
]); ]);
$job = new SyncChannelPostsJob($channel); $job = new SyncChannelPostsJob($channel);

View file

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

View file

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

View file

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

View file

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

View file

@ -16,14 +16,14 @@ class LanguageTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['short_code', 'name', 'native_name', 'is_active']; $fillableFields = ['short_code', 'name', 'native_name', 'is_active'];
$language = new Language(); $language = new Language;
$this->assertEquals($fillableFields, $language->getFillable()); $this->assertEquals($fillableFields, $language->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$language = new Language(); $language = new Language;
$this->assertEquals('languages', $language->getTable()); $this->assertEquals('languages', $language->getTable());
} }
@ -51,7 +51,7 @@ public function test_belongs_to_many_platform_instances_relationship(): void
// Attach with required platform_language_id // Attach with required platform_language_id
$language->platformInstances()->attach([ $language->platformInstances()->attach([
$instance1->id => ['platform_language_id' => 1], $instance1->id => ['platform_language_id' => 1],
$instance2->id => ['platform_language_id' => 2] $instance2->id => ['platform_language_id' => 2],
]); ]);
$instances = $language->platformInstances; $instances = $language->platformInstances;
@ -115,7 +115,7 @@ public function test_language_creation_with_explicit_values(): void
'short_code' => 'fr', 'short_code' => 'fr',
'name' => 'French', 'name' => 'French',
'native_name' => 'Français', 'native_name' => 'Français',
'is_active' => false 'is_active' => false,
]); ]);
$this->assertEquals('fr', $language->short_code); $this->assertEquals('fr', $language->short_code);
@ -139,12 +139,12 @@ public function test_language_update(): void
{ {
$language = Language::factory()->create([ $language = Language::factory()->create([
'name' => 'Original Name', 'name' => 'Original Name',
'is_active' => true 'is_active' => true,
]); ]);
$language->update([ $language->update([
'name' => 'Updated Name', 'name' => 'Updated Name',
'is_active' => false 'is_active' => false,
]); ]);
$language->refresh(); $language->refresh();
@ -208,7 +208,7 @@ public function test_language_can_have_multiple_platform_instances(): void
$language->platformInstances()->attach([ $language->platformInstances()->attach([
$instance1->id => ['platform_language_id' => 1], $instance1->id => ['platform_language_id' => 1],
$instance2->id => ['platform_language_id' => 2], $instance2->id => ['platform_language_id' => 2],
$instance3->id => ['platform_language_id' => 3] $instance3->id => ['platform_language_id' => 3],
]); ]);
$instances = $language->platformInstances; $instances = $language->platformInstances;
@ -245,13 +245,13 @@ public function test_multiple_languages_with_same_name_different_regions(): void
$englishUS = Language::factory()->create([ $englishUS = Language::factory()->create([
'short_code' => 'en-US', 'short_code' => 'en-US',
'name' => 'English (United States)', 'name' => 'English (United States)',
'native_name' => 'English' 'native_name' => 'English',
]); ]);
$englishGB = Language::factory()->create([ $englishGB = Language::factory()->create([
'short_code' => 'en-GB', 'short_code' => 'en-GB',
'name' => 'English (United Kingdom)', 'name' => 'English (United Kingdom)',
'native_name' => 'English' 'native_name' => 'English',
]); ]);
$this->assertEquals('English', $englishUS->native_name); $this->assertEquals('English', $englishUS->native_name);
@ -300,7 +300,7 @@ public function test_language_relationships_maintain_referential_integrity(): vo
// Attach instance // Attach instance
$language->platformInstances()->attach($instance->id, [ $language->platformInstances()->attach($instance->id, [
'platform_language_id' => 1, 'platform_language_id' => 1,
'is_default' => true 'is_default' => true,
]); ]);
// Verify all relationships work // Verify all relationships work

View file

@ -6,7 +6,6 @@
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase; use Tests\TestCase;
class PlatformAccountTest extends TestCase class PlatformAccountTest extends TestCase
@ -16,14 +15,14 @@ class PlatformAccountTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['platform', 'instance_url', 'username', 'password', 'settings', 'is_active', 'last_tested_at', 'status']; $fillableFields = ['platform', 'instance_url', 'username', 'password', 'settings', 'is_active', 'last_tested_at', 'status'];
$account = new PlatformAccount(); $account = new PlatformAccount;
$this->assertEquals($fillableFields, $account->getFillable()); $this->assertEquals($fillableFields, $account->getFillable());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$account = new PlatformAccount(); $account = new PlatformAccount;
$this->assertEquals('platform_accounts', $account->getTable()); $this->assertEquals('platform_accounts', $account->getTable());
} }
@ -104,7 +103,6 @@ public function test_password_encryption_is_different_each_time(): void
$this->assertNotEquals($account1->getAttributes()['password'], $account2->getAttributes()['password']); $this->assertNotEquals($account1->getAttributes()['password'], $account2->getAttributes()['password']);
} }
public function test_password_decryption_handles_corruption(): void public function test_password_decryption_handles_corruption(): void
{ {
$account = PlatformAccount::factory()->create(); $account = PlatformAccount::factory()->create();
@ -120,17 +118,17 @@ public function test_get_active_static_method(): void
// Create active and inactive accounts // Create active and inactive accounts
$activeAccount1 = PlatformAccount::factory()->create([ $activeAccount1 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$activeAccount2 = PlatformAccount::factory()->create([ $activeAccount2 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$inactiveAccount = PlatformAccount::factory()->create([ $inactiveAccount = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => false 'is_active' => false,
]); ]);
$activeAccounts = PlatformAccount::getActive(PlatformEnum::LEMMY); $activeAccounts = PlatformAccount::getActive(PlatformEnum::LEMMY);
@ -146,17 +144,17 @@ public function test_set_as_active_method(): void
// Create multiple accounts for same platform // Create multiple accounts for same platform
$account1 = PlatformAccount::factory()->create([ $account1 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$account2 = PlatformAccount::factory()->create([ $account2 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => true 'is_active' => true,
]); ]);
$account3 = PlatformAccount::factory()->create([ $account3 = PlatformAccount::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'is_active' => false 'is_active' => false,
]); ]);
// Set account3 as active // Set account3 as active
@ -182,12 +180,12 @@ public function test_belongs_to_many_channels_relationship(): void
// Attach channels with pivot data // Attach channels with pivot data
$account->channels()->attach($channel1->id, [ $account->channels()->attach($channel1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$account->channels()->attach($channel2->id, [ $account->channels()->attach($channel2->id, [
'is_active' => false, 'is_active' => false,
'priority' => 50 'priority' => 50,
]); ]);
$channels = $account->channels; $channels = $account->channels;
@ -198,12 +196,12 @@ public function test_belongs_to_many_channels_relationship(): void
// Test pivot data // Test pivot data
$channel1FromRelation = $channels->find($channel1->id); $channel1FromRelation = $channels->find($channel1->id);
$this->assertEquals(1, $channel1FromRelation->pivot->is_active); $this->assertEquals(1, $channel1FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(100, $channel1FromRelation->pivot->priority); $this->assertEquals(100, $channel1FromRelation->pivot->priority); // @phpstan-ignore property.notFound
$channel2FromRelation = $channels->find($channel2->id); $channel2FromRelation = $channels->find($channel2->id);
$this->assertEquals(0, $channel2FromRelation->pivot->is_active); $this->assertEquals(0, $channel2FromRelation->pivot->is_active); // @phpstan-ignore property.notFound
$this->assertEquals(50, $channel2FromRelation->pivot->priority); $this->assertEquals(50, $channel2FromRelation->pivot->priority); // @phpstan-ignore property.notFound
} }
public function test_active_channels_relationship(): void public function test_active_channels_relationship(): void
@ -216,17 +214,17 @@ public function test_active_channels_relationship(): void
// Attach channels // Attach channels
$account->channels()->attach($activeChannel1->id, [ $account->channels()->attach($activeChannel1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$account->channels()->attach($activeChannel2->id, [ $account->channels()->attach($activeChannel2->id, [
'is_active' => true, 'is_active' => true,
'priority' => 200 'priority' => 200,
]); ]);
$account->channels()->attach($inactiveChannel->id, [ $account->channels()->attach($inactiveChannel->id, [
'is_active' => false, 'is_active' => false,
'priority' => 150 'priority' => 150,
]); ]);
$activeChannels = $account->activeChannels; $activeChannels = $account->activeChannels;
@ -271,7 +269,7 @@ public function test_account_creation_with_explicit_values(): void
'settings' => $settings, 'settings' => $settings,
'is_active' => false, 'is_active' => false,
'last_tested_at' => $timestamp, 'last_tested_at' => $timestamp,
'status' => 'working' 'status' => 'working',
]); ]);
$this->assertEquals(PlatformEnum::LEMMY, $account->platform); $this->assertEquals(PlatformEnum::LEMMY, $account->platform);
@ -302,12 +300,12 @@ public function test_account_update(): void
{ {
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'username' => 'original_user', 'username' => 'original_user',
'is_active' => true 'is_active' => true,
]); ]);
$account->update([ $account->update([
'username' => 'updated_user', 'username' => 'updated_user',
'is_active' => false 'is_active' => false,
]); ]);
$account->refresh(); $account->refresh();
@ -339,13 +337,13 @@ public function test_account_settings_can_be_complex_structure(): void
$complexSettings = [ $complexSettings = [
'authentication' => [ 'authentication' => [
'method' => 'jwt', 'method' => 'jwt',
'timeout' => 30 'timeout' => 30,
], ],
'features' => ['posting', 'commenting'], 'features' => ['posting', 'commenting'],
'rate_limits' => [ 'rate_limits' => [
'posts_per_hour' => 10, 'posts_per_hour' => 10,
'comments_per_hour' => 50 'comments_per_hour' => 50,
] ],
]; ];
$account = PlatformAccount::factory()->create(['settings' => $complexSettings]); $account = PlatformAccount::factory()->create(['settings' => $complexSettings]);
@ -383,7 +381,7 @@ public function test_account_can_have_multiple_channels_with_different_prioritie
$account->channels()->attach([ $account->channels()->attach([
$channel1->id => ['is_active' => true, 'priority' => 300], $channel1->id => ['is_active' => true, 'priority' => 300],
$channel2->id => ['is_active' => true, 'priority' => 100], $channel2->id => ['is_active' => true, 'priority' => 100],
$channel3->id => ['is_active' => false, 'priority' => 200] $channel3->id => ['is_active' => false, 'priority' => 200],
]); ]);
$allChannels = $account->channels; $allChannels = $account->channels;
@ -394,12 +392,12 @@ public function test_account_can_have_multiple_channels_with_different_prioritie
// Test that we can access pivot data // Test that we can access pivot data
foreach ($allChannels as $channel) { foreach ($allChannels as $channel) {
$this->assertNotNull($channel->pivot->priority); $this->assertNotNull($channel->pivot->priority); // @phpstan-ignore property.notFound
$this->assertIsInt($channel->pivot->is_active); $this->assertIsInt($channel->pivot->is_active); // @phpstan-ignore property.notFound
} }
} }
public function test_password_withoutObjectCaching_prevents_caching(): void public function test_password_without_object_caching_prevents_caching(): void
{ {
$account = PlatformAccount::factory()->create(['password' => 'original']); $account = PlatformAccount::factory()->create(['password' => 'original']);

View file

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

View file

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

View file

@ -16,7 +16,7 @@ class RouteTest extends TestCase
public function test_fillable_fields(): void public function test_fillable_fields(): void
{ {
$fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority']; $fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority'];
$route = new Route(); $route = new Route;
$this->assertEquals($fillableFields, $route->getFillable()); $this->assertEquals($fillableFields, $route->getFillable());
} }
@ -30,7 +30,7 @@ public function test_casts_is_active_to_boolean(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => '1', 'is_active' => '1',
'priority' => 50 'priority' => 50,
]); ]);
$this->assertIsBool($route->is_active); $this->assertIsBool($route->is_active);
@ -45,15 +45,15 @@ public function test_casts_is_active_to_boolean(): void
public function test_primary_key_configuration(): void public function test_primary_key_configuration(): void
{ {
$route = new Route(); $route = new Route;
$this->assertNull($route->getKeyName()); $this->assertNull($route->getKeyName()); // @phpstan-ignore method.impossibleType
$this->assertFalse($route->getIncrementing()); $this->assertFalse($route->getIncrementing());
} }
public function test_table_name(): void public function test_table_name(): void
{ {
$route = new Route(); $route = new Route;
$this->assertEquals('routes', $route->getTable()); $this->assertEquals('routes', $route->getTable());
} }
@ -67,7 +67,7 @@ public function test_belongs_to_feed_relationship(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$this->assertInstanceOf(Feed::class, $route->feed); $this->assertInstanceOf(Feed::class, $route->feed);
@ -84,7 +84,7 @@ public function test_belongs_to_platform_channel_relationship(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$this->assertInstanceOf(PlatformChannel::class, $route->platformChannel); $this->assertInstanceOf(PlatformChannel::class, $route->platformChannel);
@ -101,20 +101,22 @@ public function test_has_many_keywords_relationship(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Create keywords for this route // Create keywords for this route
/** @var Keyword $keyword1 */
$keyword1 = Keyword::factory()->create([ $keyword1 = Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test1' 'keyword' => 'test1',
]); ]);
/** @var Keyword $keyword2 */
$keyword2 = Keyword::factory()->create([ $keyword2 = Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'test2' 'keyword' => 'test2',
]); ]);
// Create keyword for different route (should not be included) // Create keyword for different route (should not be included)
@ -122,7 +124,7 @@ public function test_has_many_keywords_relationship(): void
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $otherFeed->id, 'feed_id' => $otherFeed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'other' 'keyword' => 'other',
]); ]);
$keywords = $route->keywords; $keywords = $route->keywords;
@ -144,28 +146,29 @@ public function test_keywords_relationship_filters_by_feed_and_channel(): void
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Create keyword for this exact route // Create keyword for this exact route
/** @var Keyword $matchingKeyword */
$matchingKeyword = Keyword::factory()->create([ $matchingKeyword = Keyword::factory()->create([
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'keyword' => 'matching' 'keyword' => 'matching',
]); ]);
// Create keyword for same feed but different channel // Create keyword for same feed but different channel
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed1->id, 'feed_id' => $feed1->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'keyword' => 'different_channel' 'keyword' => 'different_channel',
]); ]);
// Create keyword for same channel but different feed // Create keyword for same channel but different feed
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed2->id, 'feed_id' => $feed2->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'keyword' => 'different_feed' 'keyword' => 'different_feed',
]); ]);
$keywords = $route->keywords; $keywords = $route->keywords;
@ -195,7 +198,7 @@ public function test_route_creation_with_explicit_values(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => false, 'is_active' => false,
'priority' => 75 'priority' => 75,
]); ]);
$this->assertEquals($feed->id, $route->feed_id); $this->assertEquals($feed->id, $route->feed_id);
@ -206,14 +209,15 @@ public function test_route_creation_with_explicit_values(): void
public function test_route_update(): void public function test_route_update(): void
{ {
/** @var Route $route */
$route = Route::factory()->create([ $route = Route::factory()->create([
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$route->update([ $route->update([
'is_active' => false, 'is_active' => false,
'priority' => 25 'priority' => 25,
]); ]);
$route->refresh(); $route->refresh();
@ -231,21 +235,21 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'active_keyword', 'keyword' => 'active_keyword',
'is_active' => true 'is_active' => true,
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'keyword' => 'inactive_keyword', 'keyword' => 'inactive_keyword',
'is_active' => false 'is_active' => false,
]); ]);
$keywords = $route->keywords; $keywords = $route->keywords;

View file

@ -148,8 +148,9 @@ public function test_get_with_parameters(): void
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
Http::assertSent(function ($httpRequest) use ($params) { Http::assertSent(function ($httpRequest) {
$url = $httpRequest->url(); $url = $httpRequest->url();
return str_contains($url, 'https://lemmy.world/api/v3/posts') return str_contains($url, 'https://lemmy.world/api/v3/posts')
&& str_contains($url, 'limit=10') && str_contains($url, 'limit=10')
&& str_contains($url, 'page=1'); && str_contains($url, 'page=1');

View file

@ -2,12 +2,12 @@
namespace Tests\Unit\Modules\Lemmy\Services; namespace Tests\Unit\Modules\Lemmy\Services;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Enums\PlatformEnum; use App\Enums\PlatformEnum;
use App\Modules\Lemmy\Services\LemmyApiService;
use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Tests\TestCase; use Tests\TestCase;
use Exception;
class LemmyApiServiceTest extends TestCase class LemmyApiServiceTest extends TestCase
{ {
@ -27,7 +27,7 @@ public function test_constructor_sets_instance(): void
public function test_login_with_https_success(): void public function test_login_with_https_success(): void
{ {
Http::fake([ Http::fake([
'https://lemmy.world/api/v3/user/login' => Http::response(['jwt' => 'test-token'], 200) 'https://lemmy.world/api/v3/user/login' => Http::response(['jwt' => 'test-token'], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -46,7 +46,7 @@ public function test_login_falls_back_to_http_on_https_failure(): void
{ {
Http::fake([ Http::fake([
'https://lemmy.world/api/v3/user/login' => Http::response('', 500), 'https://lemmy.world/api/v3/user/login' => Http::response('', 500),
'http://lemmy.world/api/v3/user/login' => Http::response(['jwt' => 'http-token'], 200) 'http://lemmy.world/api/v3/user/login' => Http::response(['jwt' => 'http-token'], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -60,7 +60,7 @@ public function test_login_falls_back_to_http_on_https_failure(): void
public function test_login_with_explicit_http_scheme(): void public function test_login_with_explicit_http_scheme(): void
{ {
Http::fake([ Http::fake([
'http://localhost/api/v3/user/login' => Http::response(['jwt' => 'local-token'], 200) 'http://localhost/api/v3/user/login' => Http::response(['jwt' => 'local-token'], 200),
]); ]);
$service = new LemmyApiService('http://localhost'); $service = new LemmyApiService('http://localhost');
@ -76,7 +76,7 @@ public function test_login_with_explicit_http_scheme(): void
public function test_login_with_explicit_https_scheme(): void public function test_login_with_explicit_https_scheme(): void
{ {
Http::fake([ Http::fake([
'https://secure.lemmy/api/v3/user/login' => Http::response(['jwt' => 'secure-token'], 200) 'https://secure.lemmy/api/v3/user/login' => Http::response(['jwt' => 'secure-token'], 200),
]); ]);
$service = new LemmyApiService('https://secure.lemmy'); $service = new LemmyApiService('https://secure.lemmy');
@ -92,7 +92,7 @@ public function test_login_with_explicit_https_scheme(): void
public function test_login_returns_null_on_unsuccessful_response(): void public function test_login_returns_null_on_unsuccessful_response(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response(['error' => 'Invalid credentials'], 401) '*' => Http::response(['error' => 'Invalid credentials'], 401),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -104,7 +104,7 @@ public function test_login_returns_null_on_unsuccessful_response(): void
public function test_login_handles_rate_limit_error(): void public function test_login_handles_rate_limit_error(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response('{"error":"rate_limit_error"}', 429) '*' => Http::response('{"error":"rate_limit_error"}', 429),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -118,7 +118,7 @@ public function test_login_handles_rate_limit_error(): void
public function test_login_returns_null_when_jwt_missing_from_response(): void public function test_login_returns_null_when_jwt_missing_from_response(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response(['success' => true], 200) '*' => Http::response(['success' => true], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -146,9 +146,9 @@ public function test_get_community_id_success(): void
Http::fake([ Http::fake([
'*' => Http::response([ '*' => Http::response([
'community_view' => [ 'community_view' => [
'community' => ['id' => 123] 'community' => ['id' => 123],
] ],
], 200) ], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -166,7 +166,7 @@ public function test_get_community_id_success(): void
public function test_get_community_id_throws_on_unsuccessful_response(): void public function test_get_community_id_throws_on_unsuccessful_response(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response('Not found', 404) '*' => Http::response('Not found', 404),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -180,7 +180,7 @@ public function test_get_community_id_throws_on_unsuccessful_response(): void
public function test_get_community_id_throws_when_community_not_in_response(): void public function test_get_community_id_throws_when_community_not_in_response(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response(['success' => true], 200) '*' => Http::response(['success' => true], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -201,19 +201,19 @@ public function test_sync_channel_posts_success(): void
'id' => 1, 'id' => 1,
'url' => 'https://example.com/1', 'url' => 'https://example.com/1',
'name' => 'Post 1', 'name' => 'Post 1',
'published' => '2024-01-01T00:00:00Z' 'published' => '2024-01-01T00:00:00Z',
] ],
], ],
[ [
'post' => [ 'post' => [
'id' => 2, 'id' => 2,
'url' => 'https://example.com/2', 'url' => 'https://example.com/2',
'name' => 'Post 2', 'name' => 'Post 2',
'published' => '2024-01-02T00:00:00Z' 'published' => '2024-01-02T00:00:00Z',
] ],
] ],
] ],
], 200) ], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -249,7 +249,7 @@ public function test_sync_channel_posts_success(): void
public function test_sync_channel_posts_handles_unsuccessful_response(): void public function test_sync_channel_posts_handles_unsuccessful_response(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response('Error', 500) '*' => Http::response('Error', 500),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -275,7 +275,7 @@ public function test_sync_channel_posts_handles_exception(): void
public function test_create_post_with_all_parameters(): void public function test_create_post_with_all_parameters(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response(['post_view' => ['post' => ['id' => 999]]], 200) '*' => Http::response(['post_view' => ['post' => ['id' => 999]]], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -293,6 +293,7 @@ public function test_create_post_with_all_parameters(): void
Http::assertSent(function ($request) { Http::assertSent(function ($request) {
$data = $request->data(); $data = $request->data();
return $request->url() === 'https://lemmy.world/api/v3/post' return $request->url() === 'https://lemmy.world/api/v3/post'
&& $data['name'] === 'Test Title' && $data['name'] === 'Test Title'
&& $data['body'] === 'Test Body' && $data['body'] === 'Test Body'
@ -306,7 +307,7 @@ public function test_create_post_with_all_parameters(): void
public function test_create_post_with_minimal_parameters(): void public function test_create_post_with_minimal_parameters(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response(['post_view' => ['post' => ['id' => 888]]], 200) '*' => Http::response(['post_view' => ['post' => ['id' => 888]]], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -321,6 +322,7 @@ public function test_create_post_with_minimal_parameters(): void
Http::assertSent(function ($request) { Http::assertSent(function ($request) {
$data = $request->data(); $data = $request->data();
return $request->url() === 'https://lemmy.world/api/v3/post' return $request->url() === 'https://lemmy.world/api/v3/post'
&& $data['name'] === 'Title Only' && $data['name'] === 'Title Only'
&& $data['body'] === 'Body Only' && $data['body'] === 'Body Only'
@ -334,7 +336,7 @@ public function test_create_post_with_minimal_parameters(): void
public function test_create_post_throws_on_unsuccessful_response(): void public function test_create_post_throws_on_unsuccessful_response(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response('Forbidden', 403) '*' => Http::response('Forbidden', 403),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -351,9 +353,9 @@ public function test_get_languages_success(): void
'*' => Http::response([ '*' => Http::response([
'all_languages' => [ 'all_languages' => [
['id' => 1, 'code' => 'en', 'name' => 'English'], ['id' => 1, 'code' => 'en', 'name' => 'English'],
['id' => 2, 'code' => 'fr', 'name' => 'French'] ['id' => 2, 'code' => 'fr', 'name' => 'French'],
] ],
], 200) ], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -371,7 +373,7 @@ public function test_get_languages_success(): void
public function test_get_languages_returns_empty_array_on_failure(): void public function test_get_languages_returns_empty_array_on_failure(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response('Error', 500) '*' => Http::response('Error', 500),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');
@ -395,7 +397,7 @@ public function test_get_languages_handles_exception(): void
public function test_get_languages_returns_empty_when_all_languages_missing(): void public function test_get_languages_returns_empty_when_all_languages_missing(): void
{ {
Http::fake([ Http::fake([
'*' => Http::response(['site_view' => []], 200) '*' => Http::response(['site_view' => []], 200),
]); ]);
$service = new LemmyApiService('lemmy.world'); $service = new LemmyApiService('lemmy.world');

View file

@ -2,22 +2,23 @@
namespace Tests\Unit\Modules\Lemmy\Services; namespace Tests\Unit\Modules\Lemmy\Services;
use App\Modules\Lemmy\Services\LemmyPublisher; use App\Enums\PlatformEnum;
use App\Modules\Lemmy\Services\LemmyApiService; use App\Exceptions\PlatformAuthException;
use App\Services\Auth\LemmyAuthService;
use App\Models\Article; use App\Models\Article;
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
use App\Exceptions\PlatformAuthException; use App\Modules\Lemmy\Services\LemmyApiService;
use App\Enums\PlatformEnum; use App\Modules\Lemmy\Services\LemmyPublisher;
use Illuminate\Foundation\Testing\RefreshDatabase; use App\Services\Auth\LemmyAuthService;
use Tests\TestCase;
use Mockery;
use Exception; use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class LemmyPublisherTest extends TestCase class LemmyPublisherTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
protected function tearDown(): void protected function tearDown(): void
{ {
Mockery::close(); Mockery::close();
@ -27,7 +28,7 @@ protected function tearDown(): void
public function test_constructor_initializes_api_service(): void public function test_constructor_initializes_api_service(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$publisher = new LemmyPublisher($account); $publisher = new LemmyPublisher($account);
@ -46,22 +47,22 @@ public function test_constructor_initializes_api_service(): void
public function test_publish_to_channel_with_all_data(): void public function test_publish_to_channel_with_all_data(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$article = Article::factory()->make([ $article = Article::factory()->make([
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$channel = PlatformChannel::factory()->make([ $channel = PlatformChannel::factory()->make([
'channel_id' => '42' 'channel_id' => '42',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'Test Article', 'title' => 'Test Article',
'description' => 'Test Description', 'description' => 'Test Description',
'thumbnail' => 'https://example.com/thumb.jpg', 'thumbnail' => 'https://example.com/thumb.jpg',
'language_id' => 5 'language_id' => 5,
]; ];
// Mock LemmyAuthService via service container // Mock LemmyAuthService via service container
@ -104,15 +105,15 @@ public function test_publish_to_channel_with_all_data(): void
public function test_publish_to_channel_with_minimal_data(): void public function test_publish_to_channel_with_minimal_data(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$article = Article::factory()->make([ $article = Article::factory()->make([
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$channel = PlatformChannel::factory()->make([ $channel = PlatformChannel::factory()->make([
'channel_id' => '24' 'channel_id' => '24',
]); ]);
$extractedData = []; $extractedData = [];
@ -157,21 +158,21 @@ public function test_publish_to_channel_with_minimal_data(): void
public function test_publish_to_channel_without_thumbnail(): void public function test_publish_to_channel_without_thumbnail(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$article = Article::factory()->make([ $article = Article::factory()->make([
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$channel = PlatformChannel::factory()->make([ $channel = PlatformChannel::factory()->make([
'channel_id' => '33' 'channel_id' => '33',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'No Thumbnail Article', 'title' => 'No Thumbnail Article',
'description' => 'Article without thumbnail', 'description' => 'Article without thumbnail',
'language_id' => 2 'language_id' => 2,
]; ];
// Mock LemmyAuthService // Mock LemmyAuthService
@ -213,7 +214,7 @@ public function test_publish_to_channel_without_thumbnail(): void
public function test_publish_to_channel_throws_platform_auth_exception(): void public function test_publish_to_channel_throws_platform_auth_exception(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$article = Article::factory()->make(); $article = Article::factory()->make();
@ -239,19 +240,19 @@ public function test_publish_to_channel_throws_platform_auth_exception(): void
public function test_publish_to_channel_throws_api_exception(): void public function test_publish_to_channel_throws_api_exception(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$article = Article::factory()->make([ $article = Article::factory()->make([
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$channel = PlatformChannel::factory()->make([ $channel = PlatformChannel::factory()->make([
'channel_id' => '42' 'channel_id' => '42',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'Test Article' 'title' => 'Test Article',
]; ];
// Mock LemmyAuthService via service container // Mock LemmyAuthService via service container
@ -286,19 +287,19 @@ public function test_publish_to_channel_throws_api_exception(): void
public function test_publish_to_channel_handles_string_channel_id(): void public function test_publish_to_channel_handles_string_channel_id(): void
{ {
$account = PlatformAccount::factory()->make([ $account = PlatformAccount::factory()->make([
'instance_url' => 'https://lemmy.world' 'instance_url' => 'https://lemmy.world',
]); ]);
$article = Article::factory()->make([ $article = Article::factory()->make([
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$channel = PlatformChannel::factory()->make([ $channel = PlatformChannel::factory()->make([
'channel_id' => 'string-42' 'channel_id' => 'string-42',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'Test Title' 'title' => 'Test Title',
]; ];
// Mock LemmyAuthService // Mock LemmyAuthService

View file

@ -12,7 +12,7 @@
class ArticleFetcherRssTest extends TestCase class ArticleFetcherRssTest extends TestCase
{ {
use RefreshDatabase, CreatesArticleFetcher; use CreatesArticleFetcher, RefreshDatabase;
private string $sampleRss; private string $sampleRss;

View file

@ -2,19 +2,18 @@
namespace Tests\Unit\Services; namespace Tests\Unit\Services;
use App\Services\Article\ArticleFetcher;
use App\Services\Log\LogSaver;
use App\Models\Feed;
use App\Models\Article; use App\Models\Article;
use Tests\TestCase; use App\Models\Feed;
use Tests\Traits\CreatesArticleFetcher; use App\Services\Article\ArticleFetcher;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Mockery; use Mockery;
use Tests\TestCase;
use Tests\Traits\CreatesArticleFetcher;
class ArticleFetcherTest extends TestCase class ArticleFetcherTest extends TestCase
{ {
use RefreshDatabase, CreatesArticleFetcher; use CreatesArticleFetcher, RefreshDatabase;
protected function setUp(): void protected function setUp(): void
{ {
@ -22,7 +21,7 @@ protected function setUp(): void
// Mock all HTTP requests by default to prevent external calls // Mock all HTTP requests by default to prevent external calls
Http::fake([ Http::fake([
'*' => Http::response('<html><body>Mock HTML content</body></html>', 200) '*' => Http::response('<html><body>Mock HTML content</body></html>', 200),
]); ]);
// Create ArticleFetcher only when needed - tests will create their own // Create ArticleFetcher only when needed - tests will create their own
@ -34,7 +33,7 @@ public function test_get_articles_from_feed_returns_collection(): void
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'rss', 'type' => 'rss',
'url' => 'https://example.com/feed.rss' 'url' => 'https://example.com/feed.rss',
]); ]);
$result = $articleFetcher->getArticlesFromFeed($feed); $result = $articleFetcher->getArticlesFromFeed($feed);
@ -46,7 +45,7 @@ public function test_get_articles_from_rss_feed_returns_empty_collection(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'rss', 'type' => 'rss',
'url' => 'https://example.com/feed.rss' 'url' => 'https://example.com/feed.rss',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -60,7 +59,7 @@ public function test_get_articles_from_website_feed_handles_no_parser(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'website', 'type' => 'website',
'url' => 'https://unsupported-site.com/' 'url' => 'https://unsupported-site.com/',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -75,7 +74,7 @@ public function test_get_articles_from_unsupported_feed_type(): void
{ {
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'website', // Use valid type but with unsupported URL 'type' => 'website', // Use valid type but with unsupported URL
'url' => 'https://unsupported-feed-type.com/feed' 'url' => 'https://unsupported-feed-type.com/feed',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -88,7 +87,7 @@ public function test_get_articles_from_unsupported_feed_type(): void
public function test_fetch_article_data_returns_array(): void public function test_fetch_article_data_returns_array(): void
{ {
$article = Article::factory()->create([ $article = Article::factory()->create([
'url' => 'https://example.com/article' 'url' => 'https://example.com/article',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -102,7 +101,7 @@ public function test_fetch_article_data_returns_array(): void
public function test_fetch_article_data_handles_invalid_url(): void public function test_fetch_article_data_handles_invalid_url(): void
{ {
$article = Article::factory()->create([ $article = Article::factory()->create([
'url' => 'invalid-url' 'url' => 'invalid-url',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -117,7 +116,7 @@ public function test_get_articles_from_feed_with_null_feed_type(): void
// Create feed with valid type first, then manually set to invalid value // Create feed with valid type first, then manually set to invalid value
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'website', 'type' => 'website',
'url' => 'https://example.com/feed' 'url' => 'https://example.com/feed',
]); ]);
// Use reflection to set an invalid type that bypasses enum validation // Use reflection to set an invalid type that bypasses enum validation
@ -139,12 +138,12 @@ public function test_get_articles_from_website_feed_with_supported_parser(): voi
{ {
// Mock successful HTTP response with sample HTML // Mock successful HTTP response with sample HTML
Http::fake([ Http::fake([
'https://www.vrt.be/vrtnws/nl/' => Http::response('<html><body>Sample VRT content</body></html>', 200) 'https://www.vrt.be/vrtnws/nl/' => Http::response('<html><body>Sample VRT content</body></html>', 200),
]); ]);
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'website', 'type' => 'website',
'url' => 'https://www.vrt.be/vrtnws/nl/' 'url' => 'https://www.vrt.be/vrtnws/nl/',
]); ]);
// Test actual behavior - VRT parser should be available // Test actual behavior - VRT parser should be available
@ -161,7 +160,7 @@ public function test_get_articles_from_website_feed_handles_invalid_url(): void
$feed = Feed::factory()->create([ $feed = Feed::factory()->create([
'type' => 'website', 'type' => 'website',
'url' => 'https://invalid-domain-that-does-not-exist-12345.com/' 'url' => 'https://invalid-domain-that-does-not-exist-12345.com/',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -175,11 +174,11 @@ public function test_fetch_article_data_with_supported_parser(): void
{ {
// Mock successful HTTP response with sample HTML // Mock successful HTTP response with sample HTML
Http::fake([ Http::fake([
'https://www.vrt.be/vrtnws/nl/test-article' => Http::response('<html><body>Sample article content</body></html>', 200) 'https://www.vrt.be/vrtnws/nl/test-article' => Http::response('<html><body>Sample article content</body></html>', 200),
]); ]);
$article = Article::factory()->create([ $article = Article::factory()->create([
'url' => 'https://www.vrt.be/vrtnws/nl/test-article' 'url' => 'https://www.vrt.be/vrtnws/nl/test-article',
]); ]);
// Test actual behavior - VRT parser should be available // Test actual behavior - VRT parser should be available
@ -193,7 +192,7 @@ public function test_fetch_article_data_with_supported_parser(): void
public function test_fetch_article_data_handles_unsupported_domain(): void public function test_fetch_article_data_handles_unsupported_domain(): void
{ {
$article = Article::factory()->create([ $article = Article::factory()->create([
'url' => 'https://unsupported-domain.com/article' 'url' => 'https://unsupported-domain.com/article',
]); ]);
$articleFetcher = $this->createArticleFetcher(); $articleFetcher = $this->createArticleFetcher();
@ -230,7 +229,7 @@ public function test_save_article_returns_existing_article_when_exists(): void
$feed = Feed::factory()->create(); $feed = Feed::factory()->create();
$existingArticle = Article::factory()->create([ $existingArticle = Article::factory()->create([
'url' => 'https://example.com/existing-article', 'url' => 'https://example.com/existing-article',
'feed_id' => $feed->id 'feed_id' => $feed->id,
]); ]);
// Use reflection to access private method for testing // Use reflection to access private method for testing
@ -262,7 +261,7 @@ public function test_save_article_without_feed_id(): void
$this->assertInstanceOf(Article::class, $article); $this->assertInstanceOf(Article::class, $article);
$this->assertEquals($url, $article->url); $this->assertEquals($url, $article->url);
$this->assertNull($article->feed_id); $this->assertNull($article->feed_id); // @phpstan-ignore method.impossibleType
$this->assertDatabaseHas('articles', ['url' => $url, 'feed_id' => null]); $this->assertDatabaseHas('articles', ['url' => $url, 'feed_id' => null]);
} }

View file

@ -24,13 +24,13 @@ public function test_get_token_successfully_authenticates(): void
// Mock successful HTTP response for both HTTPS and HTTP (fallback) // Mock successful HTTP response for both HTTPS and HTTP (fallback)
Http::fake([ Http::fake([
'https://lemmy.test/api/v3/user/login' => Http::response(['jwt' => 'jwt-123'], 200), 'https://lemmy.test/api/v3/user/login' => Http::response(['jwt' => 'jwt-123'], 200),
'http://lemmy.test/api/v3/user/login' => Http::response(['jwt' => 'jwt-123'], 200) 'http://lemmy.test/api/v3/user/login' => Http::response(['jwt' => 'jwt-123'], 200),
]); ]);
$account = PlatformAccount::factory()->create([ $account = PlatformAccount::factory()->create([
'username' => 'testuser', 'username' => 'testuser',
'password' => 'testpass', 'password' => 'testpass',
'instance_url' => 'https://lemmy.test' 'instance_url' => 'https://lemmy.test',
]); ]);
$result = app(LemmyAuthService::class)->getToken($account); $result = app(LemmyAuthService::class)->getToken($account);
@ -97,7 +97,7 @@ public function test_get_token_throws_exception_when_login_fails(): void
// Mock failed HTTP response for both HTTPS and HTTP // Mock failed HTTP response for both HTTPS and HTTP
Http::fake([ Http::fake([
'https://lemmy.test/api/v3/user/login' => Http::response(['error' => 'Invalid credentials'], 401), 'https://lemmy.test/api/v3/user/login' => Http::response(['error' => 'Invalid credentials'], 401),
'http://lemmy.test/api/v3/user/login' => Http::response(['error' => 'Invalid credentials'], 401) 'http://lemmy.test/api/v3/user/login' => Http::response(['error' => 'Invalid credentials'], 401),
]); ]);
$account = $this->createMock(PlatformAccount::class); $account = $this->createMock(PlatformAccount::class);
@ -121,7 +121,7 @@ public function test_get_token_throws_exception_when_login_returns_false(): void
// Mock response with empty/missing JWT for both HTTPS and HTTP // Mock response with empty/missing JWT for both HTTPS and HTTP
Http::fake([ Http::fake([
'https://lemmy.test/api/v3/user/login' => Http::response(['success' => false], 200), 'https://lemmy.test/api/v3/user/login' => Http::response(['success' => false], 200),
'http://lemmy.test/api/v3/user/login' => Http::response(['success' => false], 200) 'http://lemmy.test/api/v3/user/login' => Http::response(['success' => false], 200),
]); ]);
$account = $this->createMock(PlatformAccount::class); $account = $this->createMock(PlatformAccount::class);

View file

@ -3,8 +3,8 @@
namespace Tests\Unit\Services; namespace Tests\Unit\Services;
use App\Services\DashboardStatsService; use App\Services\DashboardStatsService;
use Tests\TestCase;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class DashboardStatsServiceTest extends TestCase class DashboardStatsServiceTest extends TestCase
{ {
@ -14,13 +14,13 @@ protected function setUp(): void
// Mock HTTP requests to prevent external calls // Mock HTTP requests to prevent external calls
Http::fake([ Http::fake([
'*' => Http::response('', 500) '*' => Http::response('', 500),
]); ]);
} }
public function test_get_available_periods_returns_correct_options(): void public function test_get_available_periods_returns_correct_options(): void
{ {
$service = new DashboardStatsService(); $service = new DashboardStatsService;
$periods = $service->getAvailablePeriods(); $periods = $service->getAvailablePeriods();
$this->assertIsArray($periods); $this->assertIsArray($periods);
@ -36,7 +36,7 @@ public function test_get_available_periods_returns_correct_options(): void
public function test_service_instantiation(): void public function test_service_instantiation(): void
{ {
$service = new DashboardStatsService(); $service = new DashboardStatsService;
$this->assertInstanceOf(DashboardStatsService::class, $service); $this->assertInstanceOf(DashboardStatsService::class, $service);
} }
} }

View file

@ -64,7 +64,8 @@ public function test_get_supported_sources_returns_sources_in_correct_order(): v
public function test_register_parser_adds_new_parser_to_list(): void public function test_register_parser_adds_new_parser_to_list(): void
{ {
// Create a mock parser class // Create a mock parser class
$mockParserClass = new class implements ArticleParserInterface { $mockParserClass = new class implements ArticleParserInterface
{
public function canParse(string $url): bool public function canParse(string $url): bool
{ {
return str_contains($url, 'test-parser.com'); return str_contains($url, 'test-parser.com');
@ -115,7 +116,8 @@ public function test_register_parser_prevents_duplicate_registration(): void
public function test_get_parser_uses_first_matching_parser(): void public function test_get_parser_uses_first_matching_parser(): void
{ {
// Create two mock parsers that can parse the same URL // Create two mock parsers that can parse the same URL
$mockParser1 = new class implements ArticleParserInterface { $mockParser1 = new class implements ArticleParserInterface
{
public function canParse(string $url): bool public function canParse(string $url): bool
{ {
return str_contains($url, 'shared-domain.com'); return str_contains($url, 'shared-domain.com');
@ -132,7 +134,8 @@ public function getSourceName(): string
} }
}; };
$mockParser2 = new class implements ArticleParserInterface { $mockParser2 = new class implements ArticleParserInterface
{
public function canParse(string $url): bool public function canParse(string $url): bool
{ {
return str_contains($url, 'shared-domain.com'); return str_contains($url, 'shared-domain.com');
@ -167,7 +170,8 @@ public function getSourceName(): string
public function test_factory_maintains_parser_registration_across_calls(): void public function test_factory_maintains_parser_registration_across_calls(): void
{ {
// Create a mock parser // Create a mock parser
$mockParser = new class implements ArticleParserInterface { $mockParser = new class implements ArticleParserInterface
{
public function canParse(string $url): bool public function canParse(string $url): bool
{ {
return str_contains($url, 'persistent-test.com'); return str_contains($url, 'persistent-test.com');

View file

@ -24,7 +24,7 @@ public function test_fetch_html_returns_response_body_on_successful_request(): v
$expectedHtml = '<html><body>Test content</body></html>'; $expectedHtml = '<html><body>Test content</body></html>';
Http::fake([ Http::fake([
$url => Http::response($expectedHtml, 200) $url => Http::response($expectedHtml, 200),
]); ]);
$result = HttpFetcher::fetchHtml($url); $result = HttpFetcher::fetchHtml($url);
@ -41,7 +41,7 @@ public function test_fetch_html_throws_exception_on_unsuccessful_response(): voi
$statusCode = 404; $statusCode = 404;
Http::fake([ Http::fake([
$url => Http::response('Not Found', $statusCode) $url => Http::response('Not Found', $statusCode),
]); ]);
$this->expectException(Exception::class); $this->expectException(Exception::class);
@ -55,7 +55,7 @@ public function test_fetch_html_logs_error_on_exception(): void
$url = 'https://example.com'; $url = 'https://example.com';
Http::fake([ Http::fake([
$url => Http::response('Server Error', 500) $url => Http::response('Server Error', 500),
]); ]);
try { try {
@ -87,7 +87,7 @@ public function test_fetch_multiple_urls_returns_successful_results(): void
{ {
$urls = [ $urls = [
'https://example.com/page1', 'https://example.com/page1',
'https://example.com/page2' 'https://example.com/page2',
]; ];
$html1 = '<html>Page 1</html>'; $html1 = '<html>Page 1</html>';
@ -95,7 +95,7 @@ public function test_fetch_multiple_urls_returns_successful_results(): void
Http::fake([ Http::fake([
'https://example.com/page1' => Http::response($html1, 200), 'https://example.com/page1' => Http::response($html1, 200),
'https://example.com/page2' => Http::response($html2, 200) 'https://example.com/page2' => Http::response($html2, 200),
]); ]);
$results = HttpFetcher::fetchMultipleUrls($urls); $results = HttpFetcher::fetchMultipleUrls($urls);
@ -105,13 +105,13 @@ public function test_fetch_multiple_urls_returns_successful_results(): void
$this->assertEquals([ $this->assertEquals([
'url' => 'https://example.com/page1', 'url' => 'https://example.com/page1',
'html' => $html1, 'html' => $html1,
'success' => true 'success' => true,
], $results[0]); ], $results[0]);
$this->assertEquals([ $this->assertEquals([
'url' => 'https://example.com/page2', 'url' => 'https://example.com/page2',
'html' => $html2, 'html' => $html2,
'success' => true 'success' => true,
], $results[1]); ], $results[1]);
} }
@ -119,14 +119,14 @@ public function test_fetch_multiple_urls_handles_mixed_success_failure(): void
{ {
$urls = [ $urls = [
'https://example.com/success', 'https://example.com/success',
'https://example.com/failure' 'https://example.com/failure',
]; ];
$successHtml = '<html>Success</html>'; $successHtml = '<html>Success</html>';
Http::fake([ Http::fake([
'https://example.com/success' => Http::response($successHtml, 200), 'https://example.com/success' => Http::response($successHtml, 200),
'https://example.com/failure' => Http::response('Not Found', 404) 'https://example.com/failure' => Http::response('Not Found', 404),
]); ]);
$results = HttpFetcher::fetchMultipleUrls($urls); $results = HttpFetcher::fetchMultipleUrls($urls);
@ -137,7 +137,7 @@ public function test_fetch_multiple_urls_handles_mixed_success_failure(): void
$this->assertEquals([ $this->assertEquals([
'url' => 'https://example.com/success', 'url' => 'https://example.com/success',
'html' => $successHtml, 'html' => $successHtml,
'success' => true 'success' => true,
], $results[0]); ], $results[0]);
// Second URL should fail // Second URL should fail
@ -145,7 +145,7 @@ public function test_fetch_multiple_urls_handles_mixed_success_failure(): void
'url' => 'https://example.com/failure', 'url' => 'https://example.com/failure',
'html' => null, 'html' => null,
'success' => false, 'success' => false,
'status' => 404 'status' => 404,
], $results[1]); ], $results[1]);
} }
@ -181,10 +181,11 @@ public function test_fetch_multiple_urls_handles_response_exception(): void
Http::fake([ Http::fake([
'https://example.com' => function () { 'https://example.com' => function () {
$response = Http::response('Success', 200); $response = Http::response('Success', 200);
// We can't easily mock an exception on the response object itself // We can't easily mock an exception on the response object itself
// so we'll test this scenario differently // so we'll test this scenario differently
return $response; return $response;
} },
]); ]);
$results = HttpFetcher::fetchMultipleUrls($urls); $results = HttpFetcher::fetchMultipleUrls($urls);
@ -198,12 +199,12 @@ public function test_fetch_multiple_urls_filters_null_results(): void
// This tests the edge case where URLs array might have gaps // This tests the edge case where URLs array might have gaps
$urls = [ $urls = [
'https://example.com/page1', 'https://example.com/page1',
'https://example.com/page2' 'https://example.com/page2',
]; ];
Http::fake([ Http::fake([
'https://example.com/page1' => Http::response('<html>Page 1</html>', 200), 'https://example.com/page1' => Http::response('<html>Page 1</html>', 200),
'https://example.com/page2' => Http::response('<html>Page 2</html>', 200) 'https://example.com/page2' => Http::response('<html>Page 2</html>', 200),
]); ]);
$results = HttpFetcher::fetchMultipleUrls($urls); $results = HttpFetcher::fetchMultipleUrls($urls);
@ -223,7 +224,7 @@ public function test_fetch_html_with_various_status_codes(int $statusCode): void
$url = 'https://example.com'; $url = 'https://example.com';
Http::fake([ Http::fake([
$url => Http::response('Error', $statusCode) $url => Http::response('Error', $statusCode),
]); ]);
$this->expectException(Exception::class); $this->expectException(Exception::class);
@ -232,10 +233,11 @@ public function test_fetch_html_with_various_status_codes(int $statusCode): void
HttpFetcher::fetchHtml($url); HttpFetcher::fetchHtml($url);
} }
/** @return array<int, array{int}> */
public static function statusCodesProvider(): array public static function statusCodesProvider(): array
{ {
return [ return [
[400], [401], [403], [404], [500], [502], [503] [400], [401], [403], [404], [500], [502], [503],
]; ];
} }
@ -244,13 +246,13 @@ public function test_fetch_multiple_urls_preserves_url_order(): void
$urls = [ $urls = [
'https://example.com/first', 'https://example.com/first',
'https://example.com/second', 'https://example.com/second',
'https://example.com/third' 'https://example.com/third',
]; ];
Http::fake([ Http::fake([
'https://example.com/first' => Http::response('First', 200), 'https://example.com/first' => Http::response('First', 200),
'https://example.com/second' => Http::response('Second', 200), 'https://example.com/second' => Http::response('Second', 200),
'https://example.com/third' => Http::response('Third', 200) 'https://example.com/third' => Http::response('Third', 200),
]); ]);
$results = HttpFetcher::fetchMultipleUrls($urls); $results = HttpFetcher::fetchMultipleUrls($urls);
@ -266,7 +268,7 @@ public function test_fetch_html_logs_correct_error_information(): void
$url = 'https://example.com/test-page'; $url = 'https://example.com/test-page';
Http::fake([ Http::fake([
$url => Http::response('Forbidden', 403) $url => Http::response('Forbidden', 403),
]); ]);
try { try {

View file

@ -20,7 +20,7 @@ class LogSaverTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->logSaver = new LogSaver(); $this->logSaver = new LogSaver;
} }
public function test_info_creates_log_record_with_info_level(): void public function test_info_creates_log_record_with_info_level(): void
@ -91,12 +91,12 @@ public function test_log_with_channel_includes_channel_information_in_context():
{ {
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.example.com' 'url' => 'https://lemmy.example.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'Test Channel', 'name' => 'Test Channel',
'platform_instance_id' => $platformInstance->id 'platform_instance_id' => $platformInstance->id,
]); ]);
$message = 'Test message with channel'; $message = 'Test message with channel';
@ -150,12 +150,12 @@ public function test_log_with_channel_but_empty_context_includes_only_channel_in
{ {
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://test.lemmy.com' 'url' => 'https://test.lemmy.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'Empty Context Channel', 'name' => 'Empty Context Channel',
'platform_instance_id' => $platformInstance->id 'platform_instance_id' => $platformInstance->id,
]); ]);
$message = 'Message with channel but no context'; $message = 'Message with channel but no context';
@ -179,18 +179,18 @@ public function test_context_merging_preserves_original_keys_and_adds_channel_in
{ {
$platformInstance = PlatformInstance::factory()->create([ $platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY, 'platform' => PlatformEnum::LEMMY,
'url' => 'https://merge.lemmy.com' 'url' => 'https://merge.lemmy.com',
]); ]);
$channel = PlatformChannel::factory()->create([ $channel = PlatformChannel::factory()->create([
'name' => 'Merge Test Channel', 'name' => 'Merge Test Channel',
'platform_instance_id' => $platformInstance->id 'platform_instance_id' => $platformInstance->id,
]); ]);
$originalContext = [ $originalContext = [
'article_id' => 123, 'article_id' => 123,
'user_action' => 'publish', 'user_action' => 'publish',
'timestamp' => '2024-01-01 12:00:00' 'timestamp' => '2024-01-01 12:00:00',
]; ];
$this->logSaver->error('Context merge test', $channel, $originalContext); $this->logSaver->error('Context merge test', $channel, $originalContext);
@ -238,11 +238,11 @@ public function test_log_with_complex_context_data(): void
$complexContext = [ $complexContext = [
'nested' => [ 'nested' => [
'array' => ['value1', 'value2'], 'array' => ['value1', 'value2'],
'object' => ['key' => 'value'] 'object' => ['key' => 'value'],
], ],
'numbers' => [1, 2, 3.14], 'numbers' => [1, 2, 3.14],
'boolean' => true, 'boolean' => true,
'null_value' => null 'null_value' => null,
]; ];
$this->logSaver->debug('Complex context test', null, $complexContext); $this->logSaver->debug('Complex context test', null, $complexContext);

View file

@ -13,7 +13,7 @@ class GuardianArticleParserTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->parser = new GuardianArticleParser(); $this->parser = new GuardianArticleParser;
} }
public function test_implements_article_parser_interface(): void public function test_implements_article_parser_interface(): void

View file

@ -5,7 +5,6 @@
use App\Enums\PlatformEnum; use App\Enums\PlatformEnum;
use App\Exceptions\PublishException; use App\Exceptions\PublishException;
use App\Models\Article; use App\Models\Article;
use App\Models\ArticlePublication;
use App\Models\Feed; use App\Models\Feed;
use App\Models\PlatformAccount; use App\Models\PlatformAccount;
use App\Models\PlatformChannel; use App\Models\PlatformChannel;
@ -19,7 +18,6 @@
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery; use Mockery;
use RuntimeException;
use Tests\TestCase; use Tests\TestCase;
class ArticlePublishingServiceTest extends TestCase class ArticlePublishingServiceTest extends TestCase
@ -27,6 +25,7 @@ class ArticlePublishingServiceTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
protected ArticlePublishingService $service; protected ArticlePublishingService $service;
protected LogSaver $logSaver; protected LogSaver $logSaver;
protected function setUp(): void protected function setUp(): void
@ -63,7 +62,7 @@ public function test_publish_to_routed_channels_returns_empty_collection_when_no
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'approval_status' => 'approved', 'approval_status' => 'approved',
'validated_at' => now() 'validated_at' => now(),
]); ]);
$extractedData = ['title' => 'Test Title']; $extractedData = ['title' => 'Test Title'];
@ -90,7 +89,7 @@ public function test_publish_to_routed_channels_skips_routes_without_active_acco
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Don't create any platform accounts for the channel // Don't create any platform accounts for the channel
@ -119,13 +118,13 @@ public function test_publish_to_routed_channels_successfully_publishes_to_channe
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Attach account to channel as active // Attach account to channel as active
$channel->platformAccounts()->attach($account->id, [ $channel->platformAccounts()->attach($account->id, [
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Mock publisher via service seam // Mock publisher via service seam
@ -166,13 +165,13 @@ public function test_publish_to_routed_channels_handles_publishing_failure_grace
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel->id, 'platform_channel_id' => $channel->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Attach account to channel as active // Attach account to channel as active
$channel->platformAccounts()->attach($account->id, [ $channel->platformAccounts()->attach($account->id, [
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Publisher throws an exception via service seam // Publisher throws an exception via service seam
@ -210,24 +209,24 @@ public function test_publish_to_routed_channels_publishes_to_multiple_routes():
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
Route::create([ Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Attach accounts to channels as active // Attach accounts to channels as active
$channel1->platformAccounts()->attach($account1->id, [ $channel1->platformAccounts()->attach($account1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$channel2->platformAccounts()->attach($account2->id, [ $channel2->platformAccounts()->attach($account2->id, [
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$publisherDouble = \Mockery::mock(LemmyPublisher::class); $publisherDouble = \Mockery::mock(LemmyPublisher::class);
@ -266,24 +265,24 @@ public function test_publish_to_routed_channels_filters_out_failed_publications(
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel1->id, 'platform_channel_id' => $channel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
Route::create([ Route::create([
'feed_id' => $feed->id, 'feed_id' => $feed->id,
'platform_channel_id' => $channel2->id, 'platform_channel_id' => $channel2->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
// Attach accounts to channels as active // Attach accounts to channels as active
$channel1->platformAccounts()->attach($account1->id, [ $channel1->platformAccounts()->attach($account1->id, [
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$channel2->platformAccounts()->attach($account2->id, [ $channel2->platformAccounts()->attach($account2->id, [
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
$publisherDouble = \Mockery::mock(LemmyPublisher::class); $publisherDouble = \Mockery::mock(LemmyPublisher::class);

View file

@ -18,10 +18,15 @@ class KeywordFilteringTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
private ArticlePublishingService $service; private ArticlePublishingService $service;
private Feed $feed; private Feed $feed;
private PlatformChannel $channel1; private PlatformChannel $channel1;
private PlatformChannel $channel2; private PlatformChannel $channel2;
private Route $route1; private Route $route1;
private Route $route2; private Route $route2;
protected function setUp(): void protected function setUp(): void
@ -43,14 +48,14 @@ protected function setUp(): void
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'is_active' => true, 'is_active' => true,
'priority' => 100 'priority' => 100,
]); ]);
$this->route2 = Route::create([ $this->route2 = Route::create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel2->id, 'platform_channel_id' => $this->channel2->id,
'is_active' => true, 'is_active' => true,
'priority' => 50 'priority' => 50,
]); ]);
} }
@ -64,13 +69,13 @@ public function test_route_with_no_keywords_matches_all_articles(): void
{ {
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'Some random article', 'title' => 'Some random article',
'description' => 'This is about something', 'description' => 'This is about something',
'full_article' => 'The content talks about various topics' 'full_article' => 'The content talks about various topics',
]; ];
// Use reflection to test private method // Use reflection to test private method
@ -90,25 +95,25 @@ public function test_route_with_keywords_matches_article_containing_keyword(): v
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'Belgium', 'keyword' => 'Belgium',
'is_active' => true 'is_active' => true,
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'politics', 'keyword' => 'politics',
'is_active' => true 'is_active' => true,
]); ]);
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'Belgium announces new policy', 'title' => 'Belgium announces new policy',
'description' => 'The government makes changes', 'description' => 'The government makes changes',
'full_article' => 'The Belgian government announced today...' 'full_article' => 'The Belgian government announced today...',
]; ];
// Use reflection to test private method // Use reflection to test private method
@ -128,25 +133,25 @@ public function test_route_with_keywords_does_not_match_article_without_keywords
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'sports', 'keyword' => 'sports',
'is_active' => true 'is_active' => true,
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'football', 'keyword' => 'football',
'is_active' => true 'is_active' => true,
]); ]);
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'Economic news update', 'title' => 'Economic news update',
'description' => 'Markets are doing well', 'description' => 'Markets are doing well',
'full_article' => 'The economy is showing strong growth this quarter...' 'full_article' => 'The economy is showing strong growth this quarter...',
]; ];
// Use reflection to test private method // Use reflection to test private method
@ -166,31 +171,31 @@ public function test_inactive_keywords_are_ignored(): void
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'Belgium', 'keyword' => 'Belgium',
'is_active' => false // Inactive 'is_active' => false, // Inactive
]); ]);
Keyword::factory()->create([ Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'politics', 'keyword' => 'politics',
'is_active' => true // Active 'is_active' => true, // Active
]); ]);
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedDataWithInactiveKeyword = [ $extractedDataWithInactiveKeyword = [
'title' => 'Belgium announces new policy', 'title' => 'Belgium announces new policy',
'description' => 'The government makes changes', 'description' => 'The government makes changes',
'full_article' => 'The Belgian government announced today...' 'full_article' => 'The Belgian government announced today...',
]; ];
$extractedDataWithActiveKeyword = [ $extractedDataWithActiveKeyword = [
'title' => 'Political changes ahead', 'title' => 'Political changes ahead',
'description' => 'Politics is changing', 'description' => 'Politics is changing',
'full_article' => 'The political landscape is shifting...' 'full_article' => 'The political landscape is shifting...',
]; ];
// Use reflection to test private method // Use reflection to test private method
@ -211,18 +216,18 @@ public function test_keyword_matching_is_case_insensitive(): void
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'BELGIUM', 'keyword' => 'BELGIUM',
'is_active' => true 'is_active' => true,
]); ]);
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'belgium news', 'title' => 'belgium news',
'description' => 'About Belgium', 'description' => 'About Belgium',
'full_article' => 'News from belgium today...' 'full_article' => 'News from belgium today...',
]; ];
// Use reflection to test private method // Use reflection to test private method
@ -241,25 +246,25 @@ public function test_keywords_match_in_title_description_and_content(): void
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel1->id, 'platform_channel_id' => $this->channel1->id,
'keyword' => 'title-word', 'keyword' => 'title-word',
'is_active' => true 'is_active' => true,
]); ]);
$keywordInDescription = Keyword::factory()->create([ $keywordInDescription = Keyword::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel2->id, 'platform_channel_id' => $this->channel2->id,
'keyword' => 'desc-word', 'keyword' => 'desc-word',
'is_active' => true 'is_active' => true,
]); ]);
$article = Article::factory()->create([ $article = Article::factory()->create([
'feed_id' => $this->feed->id, 'feed_id' => $this->feed->id,
'approval_status' => 'approved' 'approval_status' => 'approved',
]); ]);
$extractedData = [ $extractedData = [
'title' => 'This contains title-word', 'title' => 'This contains title-word',
'description' => 'This has desc-word in it', 'description' => 'This has desc-word in it',
'full_article' => 'The content has no special words' 'full_article' => 'The content has no special words',
]; ];
// Use reflection to test private method // Use reflection to test private method

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