diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..8b4c936 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,113 @@ +name: CI + +on: + push: + branches: ['release/*'] + pull_request: + branches: [main] + +jobs: + ci: + runs-on: docker + container: + image: catthehacker/ubuntu:act-latest + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + + - name: Set up PHP + uses: https://github.com/shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_sqlite, mbstring, xml, dom + coverage: pcov + + - name: Cache Composer dependencies + uses: https://data.forgejo.org/actions/cache@v4 + with: + path: ~/.composer/cache + key: composer-${{ hashFiles('composer.lock') }} + restore-keys: composer- + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Prepare environment + run: cp .env.testing .env + + - name: Lint + run: vendor/bin/pint --test + + - name: Static analysis + run: vendor/bin/phpstan analyse + + - name: Tests + run: php artisan test --coverage-clover coverage.xml --coverage-text + + - name: Parse coverage + if: github.event_name == 'pull_request' + id: coverage + run: | + COVERAGE=$(php -r ' + $xml = simplexml_load_file("coverage.xml"); + if ($xml === false || !isset($xml->project->metrics)) { + echo "0"; + exit; + } + $metrics = $xml->project->metrics; + $statements = (int) $metrics["statements"]; + $covered = (int) $metrics["coveredstatements"]; + echo $statements > 0 ? round(($covered / $statements) * 100, 2) : 0; + ') + echo "percentage=$COVERAGE" >> "$GITHUB_OUTPUT" + + - name: Comment coverage on PR + if: github.event_name == 'pull_request' + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + COVERAGE: ${{ steps.coverage.outputs.percentage }} + REPO: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + COMMIT_SHA: ${{ github.sha }} + run: | + API_URL="${SERVER_URL}/api/v1/repos/${REPO}/issues/${PR_NUMBER}/comments" + MARKER="" + + BODY="${MARKER} + ## Code Coverage Report + + | Metric | Value | + |--------|-------| + | **Line Coverage** | ${COVERAGE}% | + + _Updated by CI — commit ${COMMIT_SHA}_" + + # Find existing coverage comment + EXISTING=$(curl -sf -H "Authorization: token ${FORGEJO_TOKEN}" \ + "${API_URL}?limit=50" | \ + php -r ' + $comments = json_decode(file_get_contents("php://stdin"), true); + if (!is_array($comments)) exit; + foreach ($comments as $c) { + if (str_contains($c["body"], "")) { + echo $c["id"]; + exit; + } + } + ' || true) + + if [ -n "$EXISTING" ]; then + # Update existing comment + curl -sf -X PATCH \ + -H "Authorization: token ${FORGEJO_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(php -r 'echo json_encode(["body" => $argv[1]]);' "$BODY")" \ + "${SERVER_URL}/api/v1/repos/${REPO}/issues/comments/${EXISTING}" > /dev/null + else + # Create new comment + curl -sf -X POST \ + -H "Authorization: token ${FORGEJO_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(php -r 'echo json_encode(["body" => $argv[1]]);' "$BODY")" \ + "${API_URL}" > /dev/null + fi diff --git a/app/Actions/CreateFeedAction.php b/app/Actions/CreateFeedAction.php index ac3ac62..ab76411 100644 --- a/app/Actions/CreateFeedAction.php +++ b/app/Actions/CreateFeedAction.php @@ -15,7 +15,7 @@ public function execute(string $name, string $provider, int $languageId, ?string $url = config("feed.providers.{$provider}.languages.{$langCode}.url"); - if (!$url) { + if (! $url) { throw new InvalidArgumentException("Invalid provider and language combination: {$provider}/{$langCode}"); } diff --git a/app/Actions/CreatePlatformAccountAction.php b/app/Actions/CreatePlatformAccountAction.php index e84e37d..2cf3d76 100644 --- a/app/Actions/CreatePlatformAccountAction.php +++ b/app/Actions/CreatePlatformAccountAction.php @@ -19,7 +19,7 @@ public function __construct( */ public function execute(string $instanceDomain, string $username, string $password, string $platform = 'lemmy'): PlatformAccount { - $fullInstanceUrl = 'https://' . $instanceDomain; + $fullInstanceUrl = 'https://'.$instanceDomain; // Authenticate first — if this fails, no records are created $authResponse = $this->lemmyAuthService->authenticate($fullInstanceUrl, $username, $password); diff --git a/app/Console/Commands/FetchNewArticlesCommand.php b/app/Console/Commands/FetchNewArticlesCommand.php index c0aef79..12bab5d 100644 --- a/app/Console/Commands/FetchNewArticlesCommand.php +++ b/app/Console/Commands/FetchNewArticlesCommand.php @@ -15,13 +15,13 @@ class FetchNewArticlesCommand extends Command public function handle(): int { - if (!Setting::isArticleProcessingEnabled()) { + if (! Setting::isArticleProcessingEnabled()) { $this->info('Article processing is disabled. Article discovery skipped.'); return self::SUCCESS; } - if (!Feed::where('is_active', true)->exists()) { + if (! Feed::where('is_active', true)->exists()) { $this->info('No active feeds found. Article discovery skipped.'); return self::SUCCESS; diff --git a/app/Contracts/ArticleParserInterface.php b/app/Contracts/ArticleParserInterface.php index ab6c89f..04917ad 100644 --- a/app/Contracts/ArticleParserInterface.php +++ b/app/Contracts/ArticleParserInterface.php @@ -11,6 +11,7 @@ public function canParse(string $url): bool; /** * Extract article data from HTML + * * @return array */ public function extractData(string $html): array; @@ -19,4 +20,4 @@ public function extractData(string $html): array; * Get the source name for this parser */ public function getSourceName(): string; -} \ No newline at end of file +} diff --git a/app/Contracts/HomepageParserInterface.php b/app/Contracts/HomepageParserInterface.php index 50301d6..c28b431 100644 --- a/app/Contracts/HomepageParserInterface.php +++ b/app/Contracts/HomepageParserInterface.php @@ -11,6 +11,7 @@ public function canParse(string $url): bool; /** * Extract article URLs from homepage HTML + * * @return array */ public function extractArticleUrls(string $html): array; @@ -24,4 +25,4 @@ public function getHomepageUrl(): string; * Get the source name for this parser */ public function getSourceName(): string; -} \ No newline at end of file +} diff --git a/app/Enums/PlatformEnum.php b/app/Enums/PlatformEnum.php index 689a532..0d8331a 100644 --- a/app/Enums/PlatformEnum.php +++ b/app/Enums/PlatformEnum.php @@ -5,4 +5,18 @@ enum PlatformEnum: string { case LEMMY = 'lemmy'; -} \ No newline at end of file + + public function channelLabel(): string + { + return match ($this) { + self::LEMMY => 'Community', + }; + } + + public function channelLabelPlural(): string + { + return match ($this) { + self::LEMMY => 'Communities', + }; + } +} diff --git a/app/Events/ActionPerformed.php b/app/Events/ActionPerformed.php new file mode 100644 index 0000000..5d3b757 --- /dev/null +++ b/app/Events/ActionPerformed.php @@ -0,0 +1,18 @@ + */ + public array $context = [], + ) {} +} diff --git a/app/Events/ExceptionLogged.php b/app/Events/ExceptionLogged.php index 702612a..518be13 100644 --- a/app/Events/ExceptionLogged.php +++ b/app/Events/ExceptionLogged.php @@ -12,6 +12,5 @@ class ExceptionLogged public function __construct( public Log $log - ) { - } + ) {} } diff --git a/app/Events/ExceptionOccurred.php b/app/Events/ExceptionOccurred.php index 79ea1cd..35a415e 100644 --- a/app/Events/ExceptionOccurred.php +++ b/app/Events/ExceptionOccurred.php @@ -17,6 +17,5 @@ public function __construct( public string $message, /** @var array */ public array $context = [] - ) { - } + ) {} } diff --git a/app/Events/NewArticleFetched.php b/app/Events/NewArticleFetched.php index 3e657f9..3635391 100644 --- a/app/Events/NewArticleFetched.php +++ b/app/Events/NewArticleFetched.php @@ -11,7 +11,5 @@ class NewArticleFetched { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public Article $article) - { - } + public function __construct(public Article $article) {} } diff --git a/app/Exceptions/ChannelException.php b/app/Exceptions/ChannelException.php index 5f526b6..f04257e 100644 --- a/app/Exceptions/ChannelException.php +++ b/app/Exceptions/ChannelException.php @@ -4,6 +4,4 @@ use Exception; -class ChannelException extends Exception -{ -} +class ChannelException extends Exception {} diff --git a/app/Exceptions/PublishException.php b/app/Exceptions/PublishException.php index 250fb79..6cc7dcf 100644 --- a/app/Exceptions/PublishException.php +++ b/app/Exceptions/PublishException.php @@ -11,7 +11,7 @@ class PublishException extends Exception { public function __construct( private readonly Article $article, - private readonly PlatformEnum|null $platform, + private readonly ?PlatformEnum $platform, ?Throwable $previous = null ) { $message = "Failed to publish article #$article->id"; diff --git a/app/Exceptions/RoutingException.php b/app/Exceptions/RoutingException.php index acbc082..437aae0 100644 --- a/app/Exceptions/RoutingException.php +++ b/app/Exceptions/RoutingException.php @@ -4,6 +4,4 @@ use Exception; -class RoutingException extends Exception -{ -} +class RoutingException extends Exception {} diff --git a/app/Facades/LogSaver.php b/app/Facades/LogSaver.php index 661e618..eaa5d92 100644 --- a/app/Facades/LogSaver.php +++ b/app/Facades/LogSaver.php @@ -10,4 +10,4 @@ protected static function getFacadeAccessor() { return \App\Services\Log\LogSaver::class; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/ArticlesController.php b/app/Http/Controllers/Api/V1/ArticlesController.php index a1606c6..7fa1d97 100644 --- a/app/Http/Controllers/Api/V1/ArticlesController.php +++ b/app/Http/Controllers/Api/V1/ArticlesController.php @@ -3,13 +3,12 @@ namespace App\Http\Controllers\Api\V1; use App\Http\Resources\ArticleResource; +use App\Jobs\ArticleDiscoveryJob; use App\Models\Article; use App\Models\Setting; -use App\Jobs\ArticleDiscoveryJob; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Artisan; class ArticlesController extends BaseController { @@ -54,7 +53,7 @@ public function approve(Article $article): JsonResponse 'Article approved and queued for publishing.' ); } catch (Exception $e) { - return $this->sendError('Failed to approve article: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to approve article: '.$e->getMessage(), [], 500); } } @@ -71,7 +70,7 @@ public function reject(Article $article): JsonResponse 'Article rejected.' ); } catch (Exception $e) { - return $this->sendError('Failed to reject article: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to reject article: '.$e->getMessage(), [], 500); } } @@ -88,7 +87,7 @@ public function refresh(): JsonResponse 'Article refresh started. New articles will appear shortly.' ); } catch (Exception $e) { - return $this->sendError('Failed to start article refresh: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to start article refresh: '.$e->getMessage(), [], 500); } } } diff --git a/app/Http/Controllers/Api/V1/AuthController.php b/app/Http/Controllers/Api/V1/AuthController.php index 8f336e0..d1f76b8 100644 --- a/app/Http/Controllers/Api/V1/AuthController.php +++ b/app/Http/Controllers/Api/V1/AuthController.php @@ -5,7 +5,6 @@ use App\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; @@ -24,7 +23,7 @@ public function login(Request $request): JsonResponse $user = User::where('email', $request->email)->first(); - if (!$user || !Hash::check($request->password, $user->password)) { + if (! $user || ! Hash::check($request->password, $user->password)) { return $this->sendError('Invalid credentials', [], 401); } @@ -42,7 +41,7 @@ public function login(Request $request): JsonResponse } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (\Exception $e) { - return $this->sendError('Login failed: ' . $e->getMessage(), [], 500); + return $this->sendError('Login failed: '.$e->getMessage(), [], 500); } } @@ -78,7 +77,7 @@ public function register(Request $request): JsonResponse } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (\Exception $e) { - return $this->sendError('Registration failed: ' . $e->getMessage(), [], 500); + return $this->sendError('Registration failed: '.$e->getMessage(), [], 500); } } @@ -92,7 +91,7 @@ public function logout(Request $request): JsonResponse return $this->sendResponse(null, 'Logged out successfully'); } catch (\Exception $e) { - return $this->sendError('Logout failed: ' . $e->getMessage(), [], 500); + return $this->sendError('Logout failed: '.$e->getMessage(), [], 500); } } @@ -109,4 +108,4 @@ public function me(Request $request): JsonResponse ], ], 'User retrieved successfully'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/BaseController.php b/app/Http/Controllers/Api/V1/BaseController.php index 4c9b10b..b3c7b16 100644 --- a/app/Http/Controllers/Api/V1/BaseController.php +++ b/app/Http/Controllers/Api/V1/BaseController.php @@ -23,6 +23,8 @@ public function sendResponse(mixed $result, string $message = 'Success', int $co /** * Error response method + * + * @param array $errorMessages */ public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse { @@ -31,7 +33,7 @@ public function sendError(string $error, array $errorMessages = [], int $code = 'message' => $error, ]; - if (!empty($errorMessages)) { + if (! empty($errorMessages)) { $response['errors'] = $errorMessages; } @@ -40,6 +42,8 @@ public function sendError(string $error, array $errorMessages = [], int $code = /** * Validation error response method + * + * @param array $errors */ public function sendValidationError(array $errors): JsonResponse { @@ -61,4 +65,4 @@ public function sendUnauthorized(string $message = 'Unauthorized'): JsonResponse { return $this->sendError($message, [], 401); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/DashboardController.php b/app/Http/Controllers/Api/V1/DashboardController.php index 4410879..f6e7b8a 100644 --- a/app/Http/Controllers/Api/V1/DashboardController.php +++ b/app/Http/Controllers/Api/V1/DashboardController.php @@ -3,9 +3,6 @@ namespace App\Http\Controllers\Api\V1; use App\Models\Article; -use App\Models\Feed; -use App\Models\PlatformAccount; -use App\Models\PlatformChannel; use App\Services\DashboardStatsService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -40,8 +37,7 @@ public function stats(Request $request): JsonResponse 'current_period' => $period, ]); } catch (\Exception $e) { - throw $e; - return $this->sendError('Failed to fetch dashboard stats: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to fetch dashboard stats: '.$e->getMessage(), [], 500); } } } diff --git a/app/Http/Controllers/Api/V1/FeedsController.php b/app/Http/Controllers/Api/V1/FeedsController.php index 37f8bf1..f7ff4f0 100644 --- a/app/Http/Controllers/Api/V1/FeedsController.php +++ b/app/Http/Controllers/Api/V1/FeedsController.php @@ -10,8 +10,8 @@ use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use InvalidArgumentException; use Illuminate\Validation\ValidationException; +use InvalidArgumentException; class FeedsController extends BaseController { @@ -21,7 +21,7 @@ class FeedsController extends BaseController public function index(Request $request): JsonResponse { $perPage = min($request->get('per_page', 15), 100); - + $feeds = Feed::with(['language']) ->withCount('articles') ->orderBy('is_active', 'desc') @@ -37,7 +37,7 @@ public function index(Request $request): JsonResponse 'total' => $feeds->total(), 'from' => $feeds->firstItem(), 'to' => $feeds->lastItem(), - ] + ], ], 'Feeds retrieved successfully.'); } @@ -64,7 +64,7 @@ public function store(StoreFeedRequest $request, CreateFeedAction $createFeedAct } catch (InvalidArgumentException $e) { return $this->sendError($e->getMessage(), [], 422); } catch (Exception $e) { - return $this->sendError('Failed to create feed: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to create feed: '.$e->getMessage(), [], 500); } } @@ -97,7 +97,7 @@ public function update(UpdateFeedRequest $request, Feed $feed): JsonResponse } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (Exception $e) { - return $this->sendError('Failed to update feed: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update feed: '.$e->getMessage(), [], 500); } } @@ -114,7 +114,7 @@ public function destroy(Feed $feed): JsonResponse 'Feed deleted successfully!' ); } catch (Exception $e) { - return $this->sendError('Failed to delete feed: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to delete feed: '.$e->getMessage(), [], 500); } } @@ -124,7 +124,7 @@ public function destroy(Feed $feed): JsonResponse public function toggle(Feed $feed): JsonResponse { try { - $newStatus = !$feed->is_active; + $newStatus = ! $feed->is_active; $feed->update(['is_active' => $newStatus]); $status = $newStatus ? 'activated' : 'deactivated'; @@ -134,7 +134,7 @@ public function toggle(Feed $feed): JsonResponse "Feed {$status} successfully!" ); } catch (Exception $e) { - return $this->sendError('Failed to toggle feed status: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to toggle feed status: '.$e->getMessage(), [], 500); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/KeywordsController.php b/app/Http/Controllers/Api/V1/KeywordsController.php index e120b41..2fa08f9 100644 --- a/app/Http/Controllers/Api/V1/KeywordsController.php +++ b/app/Http/Controllers/Api/V1/KeywordsController.php @@ -62,7 +62,7 @@ public function store(Request $request, Feed $feed, PlatformChannel $channel): J } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (\Exception $e) { - return $this->sendError('Failed to create keyword: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to create keyword: '.$e->getMessage(), [], 500); } } @@ -90,7 +90,7 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel, K } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (\Exception $e) { - return $this->sendError('Failed to update keyword: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update keyword: '.$e->getMessage(), [], 500); } } @@ -112,7 +112,7 @@ public function destroy(Feed $feed, PlatformChannel $channel, Keyword $keyword): 'Keyword deleted successfully!' ); } catch (\Exception $e) { - return $this->sendError('Failed to delete keyword: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to delete keyword: '.$e->getMessage(), [], 500); } } @@ -127,7 +127,7 @@ public function toggle(Feed $feed, PlatformChannel $channel, Keyword $keyword): return $this->sendNotFound('Keyword not found for this route.'); } - $newStatus = !$keyword->is_active; + $newStatus = ! $keyword->is_active; $keyword->update(['is_active' => $newStatus]); $status = $newStatus ? 'activated' : 'deactivated'; @@ -137,7 +137,7 @@ public function toggle(Feed $feed, PlatformChannel $channel, Keyword $keyword): "Keyword {$status} successfully!" ); } catch (\Exception $e) { - return $this->sendError('Failed to toggle keyword status: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to toggle keyword status: '.$e->getMessage(), [], 500); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/LogsController.php b/app/Http/Controllers/Api/V1/LogsController.php index 7f5867c..f8163d9 100644 --- a/app/Http/Controllers/Api/V1/LogsController.php +++ b/app/Http/Controllers/Api/V1/LogsController.php @@ -49,7 +49,7 @@ public function index(Request $request): JsonResponse ], ], 'Logs retrieved successfully.'); } catch (\Exception $e) { - return $this->sendError('Failed to retrieve logs: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to retrieve logs: '.$e->getMessage(), [], 500); } } } diff --git a/app/Http/Controllers/Api/V1/OnboardingController.php b/app/Http/Controllers/Api/V1/OnboardingController.php index 1972a8f..e701cbb 100644 --- a/app/Http/Controllers/Api/V1/OnboardingController.php +++ b/app/Http/Controllers/Api/V1/OnboardingController.php @@ -31,18 +31,18 @@ public function status(): JsonResponse // 1. They haven't completed or skipped onboarding AND // 2. They don't have all required components $hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute; - $needsOnboarding = !$onboardingCompleted && !$onboardingSkipped && !$hasAllComponents; + $needsOnboarding = ! $onboardingCompleted && ! $onboardingSkipped && ! $hasAllComponents; // Determine current step $currentStep = null; if ($needsOnboarding) { - if (!$hasPlatformAccount) { + if (! $hasPlatformAccount) { $currentStep = 'platform'; - } elseif (!$hasFeed) { + } elseif (! $hasFeed) { $currentStep = 'feed'; - } elseif (!$hasChannel) { + } elseif (! $hasChannel) { $currentStep = 'channel'; - } elseif (!$hasRoute) { + } elseif (! $hasRoute) { $currentStep = 'route'; } } @@ -56,7 +56,7 @@ public function status(): JsonResponse 'has_route' => $hasRoute, 'onboarding_skipped' => $onboardingSkipped, 'onboarding_completed' => $onboardingCompleted, - 'missing_components' => !$hasAllComponents && $onboardingCompleted, + 'missing_components' => ! $hasAllComponents && $onboardingCompleted, ], 'Onboarding status retrieved successfully.'); } @@ -84,8 +84,10 @@ public function options(): JsonResponse ->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']); // Get feed providers from config - $feedProviders = collect(config('feed.providers', [])) - ->filter(fn($provider) => $provider['is_active']) + /** @var array> $providers */ + $providers = config('feed.providers', []); + $feedProviders = collect($providers) + ->filter(fn (array $provider) => $provider['is_active']) ->values(); return $this->sendResponse([ diff --git a/app/Http/Controllers/Api/V1/PlatformAccountsController.php b/app/Http/Controllers/Api/V1/PlatformAccountsController.php index 08b2ce3..6684e37 100644 --- a/app/Http/Controllers/Api/V1/PlatformAccountsController.php +++ b/app/Http/Controllers/Api/V1/PlatformAccountsController.php @@ -53,6 +53,7 @@ public function store(StorePlatformAccountRequest $request, CreatePlatformAccoun if (str_contains($e->getMessage(), 'Rate limited by')) { return $this->sendError($e->getMessage(), [], 429); } + return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422); } catch (Exception $e) { return $this->sendError('Unable to connect to the Lemmy instance. Please check the URL and try again.', [], 422); @@ -97,7 +98,7 @@ public function update(Request $request, PlatformAccount $platformAccount): Json } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (Exception $e) { - return $this->sendError('Failed to update platform account: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update platform account: '.$e->getMessage(), [], 500); } } @@ -114,7 +115,7 @@ public function destroy(PlatformAccount $platformAccount): JsonResponse 'Platform account deleted successfully!' ); } catch (Exception $e) { - return $this->sendError('Failed to delete platform account: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to delete platform account: '.$e->getMessage(), [], 500); } } @@ -131,7 +132,7 @@ public function setActive(PlatformAccount $platformAccount): JsonResponse "Set {$platformAccount->username}@{$platformAccount->instance_url} as active for {$platformAccount->platform->value}!" ); } catch (Exception $e) { - return $this->sendError('Failed to set platform account as active: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to set platform account as active: '.$e->getMessage(), [], 500); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/PlatformChannelsController.php b/app/Http/Controllers/Api/V1/PlatformChannelsController.php index 8f94aec..a72f5ed 100644 --- a/app/Http/Controllers/Api/V1/PlatformChannelsController.php +++ b/app/Http/Controllers/Api/V1/PlatformChannelsController.php @@ -5,8 +5,8 @@ use App\Actions\CreateChannelAction; use App\Http\Requests\StorePlatformChannelRequest; use App\Http\Resources\PlatformChannelResource; -use App\Models\PlatformChannel; use App\Models\PlatformAccount; +use App\Models\PlatformChannel; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -54,7 +54,7 @@ public function store(StorePlatformChannelRequest $request, CreateChannelAction } catch (RuntimeException $e) { return $this->sendError($e->getMessage(), [], 422); } catch (Exception $e) { - return $this->sendError('Failed to create platform channel: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to create platform channel: '.$e->getMessage(), [], 500); } } @@ -91,7 +91,7 @@ public function update(Request $request, PlatformChannel $platformChannel): Json } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (Exception $e) { - return $this->sendError('Failed to update platform channel: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update platform channel: '.$e->getMessage(), [], 500); } } @@ -108,7 +108,7 @@ public function destroy(PlatformChannel $platformChannel): JsonResponse 'Platform channel deleted successfully!' ); } catch (Exception $e) { - return $this->sendError('Failed to delete platform channel: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to delete platform channel: '.$e->getMessage(), [], 500); } } @@ -118,7 +118,7 @@ public function destroy(PlatformChannel $platformChannel): JsonResponse public function toggle(PlatformChannel $channel): JsonResponse { try { - $newStatus = !$channel->is_active; + $newStatus = ! $channel->is_active; $channel->update(['is_active' => $newStatus]); $status = $newStatus ? 'activated' : 'deactivated'; @@ -128,7 +128,7 @@ public function toggle(PlatformChannel $channel): JsonResponse "Platform channel {$status} successfully!" ); } catch (Exception $e) { - return $this->sendError('Failed to toggle platform channel status: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to toggle platform channel status: '.$e->getMessage(), [], 500); } } @@ -144,6 +144,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR 'priority' => 'nullable|integer|min:1|max:100', ]); + /** @var PlatformAccount $platformAccount */ $platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']); // Check if account is already attached @@ -165,7 +166,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (Exception $e) { - return $this->sendError('Failed to attach platform account: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to attach platform account: '.$e->getMessage(), [], 500); } } @@ -175,7 +176,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR public function detachAccount(PlatformChannel $channel, PlatformAccount $account): JsonResponse { try { - if (!$channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) { + if (! $channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) { return $this->sendError('Platform account is not attached to this channel.', [], 422); } @@ -186,7 +187,7 @@ public function detachAccount(PlatformChannel $channel, PlatformAccount $account 'Platform account detached from channel successfully!' ); } catch (Exception $e) { - return $this->sendError('Failed to detach platform account: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to detach platform account: '.$e->getMessage(), [], 500); } } @@ -201,7 +202,7 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount 'priority' => 'nullable|integer|min:1|max:100', ]); - if (!$channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) { + if (! $channel->platformAccounts()->where('platform_account_id', $account->id)->exists()) { return $this->sendError('Platform account is not attached to this channel.', [], 422); } @@ -218,7 +219,7 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (Exception $e) { - return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update platform account relationship: '.$e->getMessage(), [], 500); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/RoutingController.php b/app/Http/Controllers/Api/V1/RoutingController.php index 185f099..137ae63 100644 --- a/app/Http/Controllers/Api/V1/RoutingController.php +++ b/app/Http/Controllers/Api/V1/RoutingController.php @@ -52,7 +52,7 @@ public function store(StoreRouteRequest $request, CreateRouteAction $createRoute 201 ); } catch (Exception $e) { - return $this->sendError('Failed to create routing configuration: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to create routing configuration: '.$e->getMessage(), [], 500); } } @@ -62,11 +62,11 @@ public function store(StoreRouteRequest $request, CreateRouteAction $createRoute public function show(Feed $feed, PlatformChannel $channel): JsonResponse { $route = $this->findRoute($feed, $channel); - - if (!$route) { + + if (! $route) { return $this->sendNotFound('Routing configuration not found.'); } - + $route->load(['feed', 'platformChannel', 'keywords']); return $this->sendResponse( @@ -83,7 +83,7 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel): try { $route = $this->findRoute($feed, $channel); - if (!$route) { + if (! $route) { return $this->sendNotFound('Routing configuration not found.'); } @@ -103,7 +103,7 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel): } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (Exception $e) { - return $this->sendError('Failed to update routing configuration: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update routing configuration: '.$e->getMessage(), [], 500); } } @@ -115,7 +115,7 @@ public function destroy(Feed $feed, PlatformChannel $channel): JsonResponse try { $route = $this->findRoute($feed, $channel); - if (!$route) { + if (! $route) { return $this->sendNotFound('Routing configuration not found.'); } @@ -128,7 +128,7 @@ public function destroy(Feed $feed, PlatformChannel $channel): JsonResponse 'Routing configuration deleted successfully!' ); } catch (Exception $e) { - return $this->sendError('Failed to delete routing configuration: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to delete routing configuration: '.$e->getMessage(), [], 500); } } @@ -140,11 +140,11 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse try { $route = $this->findRoute($feed, $channel); - if (!$route) { + if (! $route) { return $this->sendNotFound('Routing configuration not found.'); } - $newStatus = !$route->is_active; + $newStatus = ! $route->is_active; Route::where('feed_id', $feed->id) ->where('platform_channel_id', $channel->id) ->update(['is_active' => $newStatus]); @@ -156,7 +156,7 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse "Routing configuration {$status} successfully!" ); } catch (Exception $e) { - return $this->sendError('Failed to toggle routing configuration status: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to toggle routing configuration status: '.$e->getMessage(), [], 500); } } @@ -169,4 +169,4 @@ private function findRoute(Feed $feed, PlatformChannel $channel): ?Route ->where('platform_channel_id', $channel->id) ->first(); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/SettingsController.php b/app/Http/Controllers/Api/V1/SettingsController.php index 80edb29..8dd4987 100644 --- a/app/Http/Controllers/Api/V1/SettingsController.php +++ b/app/Http/Controllers/Api/V1/SettingsController.php @@ -23,7 +23,7 @@ public function index(): JsonResponse return $this->sendResponse($settings, 'Settings retrieved successfully.'); } catch (\Exception $e) { - return $this->sendError('Failed to retrieve settings: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to retrieve settings: '.$e->getMessage(), [], 500); } } @@ -64,7 +64,7 @@ public function update(Request $request): JsonResponse } catch (ValidationException $e) { return $this->sendValidationError($e->errors()); } catch (\Exception $e) { - return $this->sendError('Failed to update settings: ' . $e->getMessage(), [], 500); + return $this->sendError('Failed to update settings: '.$e->getMessage(), [], 500); } } } diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index 784765e..3f1f3fc 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use Illuminate\Auth\Events\Verified; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Http\RedirectResponse; @@ -19,7 +20,9 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse } if ($request->user()->markEmailAsVerified()) { - event(new Verified($request->user())); + /** @var MustVerifyEmail $user */ + $user = $request->user(); + event(new Verified($user)); } return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); diff --git a/app/Http/Middleware/EnsureOnboardingComplete.php b/app/Http/Middleware/EnsureOnboardingComplete.php index b57d117..4fe4e88 100644 --- a/app/Http/Middleware/EnsureOnboardingComplete.php +++ b/app/Http/Middleware/EnsureOnboardingComplete.php @@ -26,4 +26,4 @@ public function handle(Request $request, Closure $next): Response return $next($request); } -} \ No newline at end of file +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 303786e..81c999c 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -39,4 +39,4 @@ public function share(Request $request): array // ]); } -} \ No newline at end of file +} diff --git a/app/Http/Middleware/RedirectIfOnboardingComplete.php b/app/Http/Middleware/RedirectIfOnboardingComplete.php index 190bd77..f7415b8 100644 --- a/app/Http/Middleware/RedirectIfOnboardingComplete.php +++ b/app/Http/Middleware/RedirectIfOnboardingComplete.php @@ -20,10 +20,10 @@ public function __construct( */ public function handle(Request $request, Closure $next): Response { - if (!$this->onboardingService->needsOnboarding()) { + if (! $this->onboardingService->needsOnboarding()) { return redirect()->route('dashboard'); } return $next($request); } -} \ No newline at end of file +} diff --git a/app/Http/Requests/StoreFeedRequest.php b/app/Http/Requests/StoreFeedRequest.php index ac2e533..dfc41bf 100644 --- a/app/Http/Requests/StoreFeedRequest.php +++ b/app/Http/Requests/StoreFeedRequest.php @@ -23,7 +23,7 @@ public function rules(): array 'provider' => "required|in:{$providers}", 'language_id' => 'required|exists:languages,id', 'description' => 'nullable|string', - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; } -} \ No newline at end of file +} diff --git a/app/Http/Requests/UpdateFeedRequest.php b/app/Http/Requests/UpdateFeedRequest.php index f6ad39c..891d68b 100644 --- a/app/Http/Requests/UpdateFeedRequest.php +++ b/app/Http/Requests/UpdateFeedRequest.php @@ -19,11 +19,11 @@ public function rules(): array { return [ 'name' => 'required|string|max:255', - 'url' => 'required|url|unique:feeds,url,' . ($this->route('feed') instanceof Feed ? (string)$this->route('feed')->id : (string)$this->route('feed')), + 'url' => 'required|url|unique:feeds,url,'.($this->route('feed') instanceof Feed ? (string) $this->route('feed')->id : (string) $this->route('feed')), 'type' => 'required|in:website,rss', 'language_id' => 'required|exists:languages,id', 'description' => 'nullable|string', - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; } -} \ No newline at end of file +} diff --git a/app/Http/Resources/ArticlePublicationResource.php b/app/Http/Resources/ArticlePublicationResource.php index 11a2a9b..9640f57 100644 --- a/app/Http/Resources/ArticlePublicationResource.php +++ b/app/Http/Resources/ArticlePublicationResource.php @@ -5,6 +5,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin \App\Models\ArticlePublication + */ class ArticlePublicationResource extends JsonResource { /** @@ -17,10 +20,10 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'article_id' => $this->article_id, - 'status' => $this->status, - 'published_at' => $this->published_at?->toISOString(), + 'platform' => $this->platform, + 'published_at' => $this->published_at->toISOString(), 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Resources/ArticleResource.php b/app/Http/Resources/ArticleResource.php index e336734..36dbbbf 100644 --- a/app/Http/Resources/ArticleResource.php +++ b/app/Http/Resources/ArticleResource.php @@ -6,10 +6,13 @@ use Illuminate\Http\Resources\Json\JsonResource; /** - * @property int $id + * @mixin \App\Models\Article */ class ArticleResource extends JsonResource { + /** + * @return array + */ public function toArray(Request $request): array { return [ @@ -19,12 +22,8 @@ public function toArray(Request $request): array 'title' => $this->title, 'description' => $this->description, 'is_valid' => $this->is_valid, - 'is_duplicate' => $this->is_duplicate, 'approval_status' => $this->approval_status, 'publish_status' => $this->publish_status, - 'approved_at' => $this->approved_at?->toISOString(), - 'approved_by' => $this->approved_by, - 'fetched_at' => $this->fetched_at?->toISOString(), 'validated_at' => $this->validated_at?->toISOString(), 'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null, 'created_at' => $this->created_at->toISOString(), diff --git a/app/Http/Resources/FeedResource.php b/app/Http/Resources/FeedResource.php index c220d40..1c50a90 100644 --- a/app/Http/Resources/FeedResource.php +++ b/app/Http/Resources/FeedResource.php @@ -5,6 +5,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin \App\Models\Feed + */ class FeedResource extends JsonResource { /** @@ -26,9 +29,9 @@ public function toArray(Request $request): array 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), 'articles_count' => $this->when( - $request->routeIs('api.feeds.*') && isset($this->articles_count), + $request->routeIs('api.feeds.*') && isset($this->articles_count), $this->articles_count ), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Resources/PlatformAccountResource.php b/app/Http/Resources/PlatformAccountResource.php index a32f771..6aa9c04 100644 --- a/app/Http/Resources/PlatformAccountResource.php +++ b/app/Http/Resources/PlatformAccountResource.php @@ -5,6 +5,12 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin \App\Models\PlatformAccount + */ +/** + * @mixin \App\Models\PlatformAccount + */ class PlatformAccountResource extends JsonResource { /** @@ -28,4 +34,4 @@ public function toArray(Request $request): array 'channels' => PlatformChannelResource::collection($this->whenLoaded('channels')), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Resources/PlatformChannelResource.php b/app/Http/Resources/PlatformChannelResource.php index cdebaa4..c0d3338 100644 --- a/app/Http/Resources/PlatformChannelResource.php +++ b/app/Http/Resources/PlatformChannelResource.php @@ -5,6 +5,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin \App\Models\PlatformChannel + */ class PlatformChannelResource extends JsonResource { /** @@ -30,4 +33,4 @@ public function toArray(Request $request): array 'routes' => RouteResource::collection($this->whenLoaded('routes')), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Resources/PlatformInstanceResource.php b/app/Http/Resources/PlatformInstanceResource.php index 3f708e4..c95f1f4 100644 --- a/app/Http/Resources/PlatformInstanceResource.php +++ b/app/Http/Resources/PlatformInstanceResource.php @@ -5,6 +5,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin \App\Models\PlatformInstance + */ class PlatformInstanceResource extends JsonResource { /** @@ -24,4 +27,4 @@ public function toArray(Request $request): array 'updated_at' => $this->updated_at->toISOString(), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Resources/RouteResource.php b/app/Http/Resources/RouteResource.php index 6a02c8d..f294f1a 100644 --- a/app/Http/Resources/RouteResource.php +++ b/app/Http/Resources/RouteResource.php @@ -5,6 +5,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin \App\Models\Route + */ class RouteResource extends JsonResource { /** @@ -15,7 +18,6 @@ class RouteResource extends JsonResource public function toArray(Request $request): array { return [ - 'id' => $this->id, 'feed_id' => $this->feed_id, 'platform_channel_id' => $this->platform_channel_id, 'is_active' => $this->is_active, @@ -35,4 +37,4 @@ public function toArray(Request $request): array }), ]; } -} \ No newline at end of file +} diff --git a/app/Jobs/ArticleDiscoveryForFeedJob.php b/app/Jobs/ArticleDiscoveryForFeedJob.php index db494a6..2c94b7c 100644 --- a/app/Jobs/ArticleDiscoveryForFeedJob.php +++ b/app/Jobs/ArticleDiscoveryForFeedJob.php @@ -25,7 +25,7 @@ public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher): void $logSaver->info('Starting feed article fetch', null, [ 'feed_id' => $this->feed->id, 'feed_name' => $this->feed->name, - 'feed_url' => $this->feed->url + 'feed_url' => $this->feed->url, ]); $articles = $articleFetcher->getArticlesFromFeed($this->feed); @@ -33,7 +33,7 @@ public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher): void $logSaver->info('Feed article fetch completed', null, [ 'feed_id' => $this->feed->id, 'feed_name' => $this->feed->name, - 'articles_count' => $articles->count() + 'articles_count' => $articles->count(), ]); $this->feed->update(['last_fetched_at' => now()]); @@ -42,7 +42,7 @@ public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher): void public static function dispatchForAllActiveFeeds(): void { $logSaver = app(LogSaver::class); - + Feed::where('is_active', true) ->get() ->each(function (Feed $feed, $index) use ($logSaver) { @@ -56,7 +56,7 @@ public static function dispatchForAllActiveFeeds(): void $logSaver->info('Dispatched feed discovery job', null, [ 'feed_id' => $feed->id, 'feed_name' => $feed->name, - 'delay_minutes' => $delayMinutes + 'delay_minutes' => $delayMinutes, ]); }); } diff --git a/app/Jobs/ArticleDiscoveryJob.php b/app/Jobs/ArticleDiscoveryJob.php index c89894e..ece1f70 100644 --- a/app/Jobs/ArticleDiscoveryJob.php +++ b/app/Jobs/ArticleDiscoveryJob.php @@ -18,7 +18,7 @@ public function __construct() public function handle(LogSaver $logSaver): void { - if (!Setting::isArticleProcessingEnabled()) { + if (! Setting::isArticleProcessingEnabled()) { $logSaver->info('Article processing is disabled. Article discovery skipped.'); return; diff --git a/app/Jobs/PublishNextArticleJob.php b/app/Jobs/PublishNextArticleJob.php index f62c857..96c6cda 100644 --- a/app/Jobs/PublishNextArticleJob.php +++ b/app/Jobs/PublishNextArticleJob.php @@ -2,6 +2,8 @@ namespace App\Jobs; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Exceptions\PublishException; use App\Models\Article; use App\Models\ArticlePublication; @@ -12,7 +14,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; -class PublishNextArticleJob implements ShouldQueue, ShouldBeUnique +class PublishNextArticleJob implements ShouldBeUnique, ShouldQueue { use Queueable; @@ -28,6 +30,7 @@ public function __construct() /** * Execute the job. + * * @throws PublishException */ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService $publishingService): void @@ -52,11 +55,11 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService return; } - logger()->info('Publishing next article from scheduled job', [ + ActionPerformed::dispatch('Publishing next article from scheduled job', LogLevelEnum::INFO, [ 'article_id' => $article->id, 'title' => $article->title, 'url' => $article->url, - 'created_at' => $article->created_at + 'created_at' => $article->created_at, ]); // Fetch article data @@ -64,18 +67,18 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService try { $publishingService->publishToRoutedChannels($article, $extractedData); - - logger()->info('Successfully published article', [ + + ActionPerformed::dispatch('Successfully published article', LogLevelEnum::INFO, [ 'article_id' => $article->id, - 'title' => $article->title + 'title' => $article->title, ]); } catch (PublishException $e) { - logger()->error('Failed to publish article', [ + ActionPerformed::dispatch('Failed to publish article', LogLevelEnum::ERROR, [ 'article_id' => $article->id, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); - + throw $e; } } -} \ No newline at end of file +} diff --git a/app/Jobs/SyncChannelPostsJob.php b/app/Jobs/SyncChannelPostsJob.php index 2f5bee6..3ded064 100644 --- a/app/Jobs/SyncChannelPostsJob.php +++ b/app/Jobs/SyncChannelPostsJob.php @@ -15,7 +15,7 @@ use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Cache; -class SyncChannelPostsJob implements ShouldQueue, ShouldBeUnique +class SyncChannelPostsJob implements ShouldBeUnique, ShouldQueue { use Queueable; @@ -28,7 +28,7 @@ public function __construct( public static function dispatchForAllActiveChannels(): void { $logSaver = app(LogSaver::class); - + PlatformChannel::with(['platformInstance', 'platformAccounts']) ->whereHas('platformInstance', fn ($query) => $query->where('platform', PlatformEnum::LEMMY)) ->whereHas('platformAccounts', fn ($query) => $query->where('platform_accounts.is_active', true)) @@ -78,7 +78,7 @@ private function syncLemmyChannelPosts(LogSaver $logSaver): void } catch (Exception $e) { $logSaver->error('Failed to sync channel posts', $this->channel, [ - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); throw $e; @@ -97,13 +97,13 @@ private function getAuthToken(LemmyApiService $api, PlatformAccount $account): s return $cachedToken; } - if (!$account->username || !$account->password) { + if (! $account->username || ! $account->password) { throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials for account'); } $token = $api->login($account->username, $account->password); - if (!$token) { + if (! $token) { throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account'); } diff --git a/app/Listeners/LogActionListener.php b/app/Listeners/LogActionListener.php new file mode 100644 index 0000000..be5eab8 --- /dev/null +++ b/app/Listeners/LogActionListener.php @@ -0,0 +1,21 @@ +logSaver->log($event->level, $event->message, context: $event->context); + } catch (Exception $e) { + error_log('Failed to log action to database: '.$e->getMessage()); + } + } +} diff --git a/app/Listeners/LogExceptionToDatabase.php b/app/Listeners/LogExceptionToDatabase.php index 3ccfa07..599e251 100644 --- a/app/Listeners/LogExceptionToDatabase.php +++ b/app/Listeners/LogExceptionToDatabase.php @@ -5,14 +5,14 @@ use App\Events\ExceptionLogged; use App\Events\ExceptionOccurred; use App\Models\Log; + class LogExceptionToDatabase { - public function handle(ExceptionOccurred $event): void { // Truncate the message to prevent database errors - $message = strlen($event->message) > 255 - ? substr($event->message, 0, 252) . '...' + $message = strlen($event->message) > 255 + ? substr($event->message, 0, 252).'...' : $event->message; try { @@ -24,15 +24,15 @@ public function handle(ExceptionOccurred $event): void 'file' => $event->exception->getFile(), 'line' => $event->exception->getLine(), 'trace' => $event->exception->getTraceAsString(), - ...$event->context - ] + ...$event->context, + ], ]); ExceptionLogged::dispatch($log); } catch (\Exception $e) { // Prevent infinite recursion by not logging this exception // Optionally log to file or other non-database destination - error_log("Failed to log exception to database: " . $e->getMessage()); + error_log('Failed to log exception to database: '.$e->getMessage()); } } } diff --git a/app/Listeners/PublishApprovedArticleListener.php b/app/Listeners/PublishApprovedArticleListener.php index e3207e6..f957809 100644 --- a/app/Listeners/PublishApprovedArticleListener.php +++ b/app/Listeners/PublishApprovedArticleListener.php @@ -2,6 +2,8 @@ namespace App\Listeners; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Events\ArticleApproved; use App\Services\Article\ArticleFetcher; use App\Services\Publishing\ArticlePublishingService; @@ -40,14 +42,14 @@ public function handle(ArticleApproved $event): void if ($publications->isNotEmpty()) { $article->update(['publish_status' => 'published']); - logger()->info('Published approved article', [ + ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [ 'article_id' => $article->id, 'title' => $article->title, ]); } else { $article->update(['publish_status' => 'error']); - logger()->warning('No publications created for approved article', [ + ActionPerformed::dispatch('No publications created for approved article', LogLevelEnum::WARNING, [ 'article_id' => $article->id, 'title' => $article->title, ]); @@ -55,7 +57,7 @@ public function handle(ArticleApproved $event): void } catch (Exception $e) { $article->update(['publish_status' => 'error']); - logger()->error('Failed to publish approved article', [ + ActionPerformed::dispatch('Failed to publish approved article', LogLevelEnum::ERROR, [ 'article_id' => $article->id, 'error' => $e->getMessage(), ]); diff --git a/app/Listeners/ValidateArticleListener.php b/app/Listeners/ValidateArticleListener.php index 3b1352c..0ef3d9b 100644 --- a/app/Listeners/ValidateArticleListener.php +++ b/app/Listeners/ValidateArticleListener.php @@ -2,6 +2,8 @@ namespace App\Listeners; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Events\NewArticleFetched; use App\Models\Setting; use App\Services\Article\ValidationService; @@ -37,10 +39,11 @@ public function handle(NewArticleFetched $event): void try { $article = $this->validationService->validate($article); } catch (Exception $e) { - logger()->error('Article validation failed', [ + ActionPerformed::dispatch('Article validation failed', LogLevelEnum::ERROR, [ 'article_id' => $article->id, 'error' => $e->getMessage(), ]); + return; } diff --git a/app/Livewire/Articles.php b/app/Livewire/Articles.php index 6e603be..8d2de63 100644 --- a/app/Livewire/Articles.php +++ b/app/Livewire/Articles.php @@ -2,9 +2,9 @@ namespace App\Livewire; +use App\Jobs\ArticleDiscoveryJob; use App\Models\Article; use App\Models\Setting; -use App\Jobs\ArticleDiscoveryJob; use Livewire\Component; use Livewire\WithPagination; @@ -39,7 +39,7 @@ public function refresh(): void $this->dispatch('refresh-started'); } - public function render() + public function render(): \Illuminate\Contracts\View\View { $articles = Article::with(['feed', 'articlePublication']) ->orderBy('created_at', 'desc') diff --git a/app/Livewire/Channels.php b/app/Livewire/Channels.php index c9a41ff..a4aff29 100644 --- a/app/Livewire/Channels.php +++ b/app/Livewire/Channels.php @@ -13,7 +13,7 @@ class Channels extends Component public function toggle(int $channelId): void { $channel = PlatformChannel::findOrFail($channelId); - $channel->is_active = !$channel->is_active; + $channel->is_active = ! $channel->is_active; $channel->save(); } @@ -29,13 +29,13 @@ public function closeAccountModal(): void public function attachAccount(int $accountId): void { - if (!$this->managingChannelId) { + if (! $this->managingChannelId) { return; } $channel = PlatformChannel::findOrFail($this->managingChannelId); - if (!$channel->platformAccounts()->where('platform_account_id', $accountId)->exists()) { + if (! $channel->platformAccounts()->where('platform_account_id', $accountId)->exists()) { $channel->platformAccounts()->attach($accountId, [ 'is_active' => true, 'priority' => 1, @@ -51,7 +51,7 @@ public function detachAccount(int $channelId, int $accountId): void $channel->platformAccounts()->detach($accountId); } - public function render() + public function render(): \Illuminate\Contracts\View\View { $channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get(); $allAccounts = PlatformAccount::where('is_active', true)->get(); @@ -61,7 +61,7 @@ public function render() : null; $availableAccounts = $managingChannel - ? $allAccounts->filter(fn($account) => !$managingChannel->platformAccounts->contains('id', $account->id)) + ? $allAccounts->filter(fn ($account) => ! $managingChannel->platformAccounts->contains('id', $account->id)) : collect(); return view('livewire.channels', [ diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 66e30b1..0ffb4e9 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -19,7 +19,7 @@ public function setPeriod(string $period): void $this->period = $period; } - public function render() + public function render(): \Illuminate\Contracts\View\View { $service = app(DashboardStatsService::class); diff --git a/app/Livewire/Feeds.php b/app/Livewire/Feeds.php index e2f9ae7..89f8b42 100644 --- a/app/Livewire/Feeds.php +++ b/app/Livewire/Feeds.php @@ -10,11 +10,11 @@ class Feeds extends Component public function toggle(int $feedId): void { $feed = Feed::findOrFail($feedId); - $feed->is_active = !$feed->is_active; + $feed->is_active = ! $feed->is_active; $feed->save(); } - public function render() + public function render(): \Illuminate\Contracts\View\View { $feeds = Feed::orderBy('name')->get(); diff --git a/app/Livewire/Onboarding.php b/app/Livewire/Onboarding.php index c568a43..9c17fcf 100644 --- a/app/Livewire/Onboarding.php +++ b/app/Livewire/Onboarding.php @@ -29,36 +29,54 @@ class Onboarding extends Component // Platform form public string $instanceUrl = ''; + public string $username = ''; + public string $password = ''; + + /** @var array|null */ public ?array $existingAccount = null; // Feed form public string $feedName = ''; + public string $feedProvider = 'vrt'; + public ?int $feedLanguageId = null; + public string $feedDescription = ''; // Channel form public string $channelName = ''; + public ?int $platformInstanceId = null; + public ?int $channelLanguageId = null; + public string $channelDescription = ''; // Route form public ?int $routeFeedId = null; + public ?int $routeChannelId = null; + public int $routePriority = 50; // State + /** @var array */ public array $formErrors = []; + public bool $isLoading = false; + #[\Livewire\Attributes\Locked] public ?int $previousChannelLanguageId = null; protected CreatePlatformAccountAction $createPlatformAccountAction; + protected CreateFeedAction $createFeedAction; + protected CreateChannelAction $createChannelAction; + protected CreateRouteAction $createRouteAction; public function boot( @@ -188,7 +206,7 @@ public function createPlatformAccount(): void } } catch (Exception $e) { logger()->error('Lemmy platform account creation failed', [ - 'instance_url' => 'https://' . $this->instanceUrl, + 'instance_url' => 'https://'.$this->instanceUrl, 'username' => $this->username, 'error' => $e->getMessage(), 'class' => get_class($e), @@ -319,17 +337,20 @@ public function completeOnboarding(): void /** * Get language codes that have at least one active provider. */ + /** + * @return list + */ public function getAvailableLanguageCodes(): array { $providers = config('feed.providers', []); $languageCodes = []; foreach ($providers as $provider) { - if (!($provider['is_active'] ?? false)) { + if (! ($provider['is_active'] ?? false)) { continue; } foreach (array_keys($provider['languages'] ?? []) as $code) { - $languageCodes[$code] = true; + $languageCodes[(string) $code] = true; } } @@ -339,14 +360,17 @@ public function getAvailableLanguageCodes(): array /** * Get providers available for the current channel language. */ + /** + * @return array> + */ public function getProvidersForLanguage(): array { - if (!$this->channelLanguageId) { + if (! $this->channelLanguageId) { return []; } $language = Language::find($this->channelLanguageId); - if (!$language) { + if (! $language) { return []; } @@ -355,7 +379,7 @@ public function getProvidersForLanguage(): array $available = []; foreach ($providers as $key => $provider) { - if (!($provider['is_active'] ?? false)) { + if (! ($provider['is_active'] ?? false)) { continue; } if (isset($provider['languages'][$langCode])) { @@ -375,13 +399,14 @@ public function getProvidersForLanguage(): array */ public function getChannelLanguage(): ?Language { - if (!$this->channelLanguageId) { + if (! $this->channelLanguageId) { return null; } + return Language::find($this->channelLanguageId); } - public function render() + public function render(): \Illuminate\Contracts\View\View { // For channel step: only show languages that have providers $availableCodes = $this->getAvailableLanguageCodes(); diff --git a/app/Livewire/Routes.php b/app/Livewire/Routes.php index 29e3764..485a06b 100644 --- a/app/Livewire/Routes.php +++ b/app/Livewire/Routes.php @@ -11,12 +11,16 @@ class Routes extends Component { public bool $showCreateModal = false; + public ?int $editingFeedId = null; + public ?int $editingChannelId = null; // Create form public ?int $newFeedId = null; + public ?int $newChannelId = null; + public int $newPriority = 50; // Edit form @@ -24,6 +28,7 @@ class Routes extends Component // Keyword management public string $newKeyword = ''; + public bool $showKeywordInput = false; public function openCreateModal(): void @@ -53,6 +58,7 @@ public function createRoute(): void if ($exists) { $this->addError('newFeedId', 'This route already exists.'); + return; } @@ -87,7 +93,7 @@ public function closeEditModal(): void public function updateRoute(): void { - if (!$this->editingFeedId || !$this->editingChannelId) { + if (! $this->editingFeedId || ! $this->editingChannelId) { return; } @@ -108,7 +114,7 @@ public function toggle(int $feedId, int $channelId): void ->where('platform_channel_id', $channelId) ->firstOrFail(); - $route->is_active = !$route->is_active; + $route->is_active = ! $route->is_active; $route->save(); } @@ -126,7 +132,7 @@ public function delete(int $feedId, int $channelId): void public function addKeyword(): void { - if (!$this->editingFeedId || !$this->editingChannelId || empty(trim($this->newKeyword))) { + if (! $this->editingFeedId || ! $this->editingChannelId || empty(trim($this->newKeyword))) { return; } @@ -144,7 +150,7 @@ public function addKeyword(): void public function toggleKeyword(int $keywordId): void { $keyword = Keyword::findOrFail($keywordId); - $keyword->is_active = !$keyword->is_active; + $keyword->is_active = ! $keyword->is_active; $keyword->save(); } @@ -153,22 +159,23 @@ public function deleteKeyword(int $keywordId): void Keyword::destroy($keywordId); } - public function render() + public function render(): \Illuminate\Contracts\View\View { - $routes = Route::with(['feed', 'platformChannel']) + $routes = Route::with(['feed', 'platformChannel.platformInstance']) ->orderBy('priority', 'desc') ->get(); // Batch load keywords for all routes to avoid N+1 queries - $routeKeys = $routes->map(fn($r) => $r->feed_id . '-' . $r->platform_channel_id); + $routeKeys = $routes->map(fn ($r) => $r->feed_id.'-'.$r->platform_channel_id); $allKeywords = Keyword::whereIn('feed_id', $routes->pluck('feed_id')) ->whereIn('platform_channel_id', $routes->pluck('platform_channel_id')) ->get() - ->groupBy(fn($k) => $k->feed_id . '-' . $k->platform_channel_id); + ->groupBy(fn ($k) => $k->feed_id.'-'.$k->platform_channel_id); $routes = $routes->map(function ($route) use ($allKeywords) { - $key = $route->feed_id . '-' . $route->platform_channel_id; - $route->keywords = $allKeywords->get($key, collect()); + $key = $route->feed_id.'-'.$route->platform_channel_id; + $route->setRelation('keywords', $allKeywords->get($key, collect())); + return $route; }); @@ -179,7 +186,7 @@ public function render() $editingKeywords = collect(); if ($this->editingFeedId && $this->editingChannelId) { - $editingRoute = Route::with(['feed', 'platformChannel']) + $editingRoute = Route::with(['feed', 'platformChannel.platformInstance']) ->where('feed_id', $this->editingFeedId) ->where('platform_channel_id', $this->editingChannelId) ->first(); diff --git a/app/Livewire/Settings.php b/app/Livewire/Settings.php index 9502fbd..9e89d9b 100644 --- a/app/Livewire/Settings.php +++ b/app/Livewire/Settings.php @@ -8,10 +8,13 @@ class Settings extends Component { public bool $articleProcessingEnabled = true; + public bool $publishingApprovalsEnabled = false; + public int $articlePublishingInterval = 5; public ?string $successMessage = null; + public ?string $errorMessage = null; public function mount(): void @@ -23,14 +26,14 @@ public function mount(): void public function toggleArticleProcessing(): void { - $this->articleProcessingEnabled = !$this->articleProcessingEnabled; + $this->articleProcessingEnabled = ! $this->articleProcessingEnabled; Setting::setArticleProcessingEnabled($this->articleProcessingEnabled); $this->showSuccess(); } public function togglePublishingApprovals(): void { - $this->publishingApprovalsEnabled = !$this->publishingApprovalsEnabled; + $this->publishingApprovalsEnabled = ! $this->publishingApprovalsEnabled; Setting::setPublishingApprovalsEnabled($this->publishingApprovalsEnabled); $this->showSuccess(); } @@ -60,7 +63,7 @@ public function clearMessages(): void $this->errorMessage = null; } - public function render() + public function render(): \Illuminate\Contracts\View\View { return view('livewire.settings')->layout('layouts.app'); } diff --git a/app/Models/Article.php b/app/Models/Article.php index d508e79..59477d9 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -15,15 +15,20 @@ * @method static firstOrCreate(array $array) * @method static where(string $string, string $url) * @method static create(array $array) - * @property integer $id + * + * @property int $id * @property int $feed_id * @property Feed $feed * @property string $url + * @property string $title + * @property string|null $description + * @property string $approval_status + * @property string $publish_status * @property bool|null $is_valid * @property Carbon|null $validated_at * @property Carbon $created_at * @property Carbon $updated_at - * @property ArticlePublication $articlePublication + * @property ArticlePublication|null $articlePublication */ class Article extends Model { @@ -79,7 +84,7 @@ public function isRejected(): bool return $this->approval_status === 'rejected'; } - public function approve(string $approvedBy = null): void + public function approve(?string $approvedBy = null): void { $this->update([ 'approval_status' => 'approved', @@ -89,7 +94,7 @@ public function approve(string $approvedBy = null): void event(new ArticleApproved($this)); } - public function reject(string $rejectedBy = null): void + public function reject(?string $rejectedBy = null): void { $this->update([ 'approval_status' => 'rejected', @@ -98,12 +103,12 @@ public function reject(string $rejectedBy = null): void public function canBePublished(): bool { - if (!$this->isValid()) { + if (! $this->isValid()) { return false; } // If approval system is disabled, auto-approve valid articles - if (!\App\Models\Setting::isPublishingApprovalsEnabled()) { + if (! \App\Models\Setting::isPublishingApprovalsEnabled()) { return true; } diff --git a/app/Models/ArticlePublication.php b/app/Models/ArticlePublication.php index 4f12b9d..ee29b72 100644 --- a/app/Models/ArticlePublication.php +++ b/app/Models/ArticlePublication.php @@ -8,9 +8,16 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; /** - * @property integer $article_id - * @property integer $platform_channel_id - * @property integer $post_id + * @property int $id + * @property int $article_id + * @property int $platform_channel_id + * @property string $post_id + * @property string $platform + * @property string $published_by + * @property array|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 $array) */ @@ -18,7 +25,7 @@ class ArticlePublication extends Model { /** @use HasFactory */ use HasFactory; - + protected $fillable = [ 'article_id', 'platform_channel_id', diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 6fefbec..6b34237 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -16,7 +16,7 @@ * @property string $url * @property string $type * @property string $provider - * @property int $language_id + * @property int|null $language_id * @property Language|null $language * @property string $description * @property array $settings @@ -24,6 +24,7 @@ * @property Carbon|null $last_fetched_at * @property Carbon $created_at * @property Carbon $updated_at + * * @method static orderBy(string $string, string $string1) * @method static where(string $string, true $true) * @method static findOrFail(mixed $feed_id) @@ -32,7 +33,9 @@ class Feed extends Model { /** @use HasFactory */ use HasFactory; + private const RECENT_FETCH_THRESHOLD_HOURS = 2; + private const DAILY_FETCH_THRESHOLD_HOURS = 24; protected $fillable = [ @@ -44,13 +47,13 @@ class Feed extends Model 'description', 'settings', 'is_active', - 'last_fetched_at' + 'last_fetched_at', ]; protected $casts = [ 'settings' => 'array', 'is_active' => 'boolean', - 'last_fetched_at' => 'datetime' + 'last_fetched_at' => 'datetime', ]; public function getTypeDisplayAttribute(): string @@ -64,11 +67,11 @@ public function getTypeDisplayAttribute(): string public function getStatusAttribute(): string { - if (!$this->is_active) { + if (! $this->is_active) { return 'Inactive'; } - if (!$this->last_fetched_at) { + if (! $this->last_fetched_at) { return 'Never fetched'; } @@ -79,12 +82,12 @@ public function getStatusAttribute(): string } elseif ($hoursAgo < self::DAILY_FETCH_THRESHOLD_HOURS) { return "Fetched {$hoursAgo}h ago"; } else { - return "Fetched " . $this->last_fetched_at->diffForHumans(); + return 'Fetched '.$this->last_fetched_at->diffForHumans(); } } /** - * @return BelongsToMany + * @return BelongsToMany */ public function channels(): BelongsToMany { @@ -94,7 +97,7 @@ public function channels(): BelongsToMany } /** - * @return BelongsToMany + * @return BelongsToMany */ public function activeChannels(): BelongsToMany { diff --git a/app/Models/Keyword.php b/app/Models/Keyword.php index 616d184..5ae04c6 100644 --- a/app/Models/Keyword.php +++ b/app/Models/Keyword.php @@ -2,6 +2,7 @@ namespace App\Models; +use Database\Factories\KeywordFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -20,17 +21,18 @@ */ class Keyword extends Model { + /** @use HasFactory */ use HasFactory; protected $fillable = [ 'feed_id', 'platform_channel_id', 'keyword', - 'is_active' + 'is_active', ]; protected $casts = [ - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; /** @@ -48,5 +50,4 @@ public function platformChannel(): BelongsTo { return $this->belongsTo(PlatformChannel::class); } - } diff --git a/app/Models/Language.php b/app/Models/Language.php index f95f47c..c8f5476 100644 --- a/app/Models/Language.php +++ b/app/Models/Language.php @@ -15,13 +15,13 @@ class Language extends Model protected $fillable = [ 'short_code', - 'name', + 'name', 'native_name', - 'is_active' + 'is_active', ]; protected $casts = [ - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; /** diff --git a/app/Models/Log.php b/app/Models/Log.php index 61de42b..2d03b64 100644 --- a/app/Models/Log.php +++ b/app/Models/Log.php @@ -3,12 +3,14 @@ namespace App\Models; use App\Enums\LogLevelEnum; +use Database\Factories\LogFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; /** - * @method static create(array $array) + * @method static create(array $array) + * * @property LogLevelEnum $level * @property string $message * @property array $context @@ -17,6 +19,7 @@ */ class Log extends Model { + /** @use HasFactory */ use HasFactory; protected $table = 'logs'; diff --git a/app/Models/PlatformAccount.php b/app/Models/PlatformAccount.php index ca309e9..856bc65 100644 --- a/app/Models/PlatformAccount.php +++ b/app/Models/PlatformAccount.php @@ -2,15 +2,15 @@ namespace App\Models; +use App\Enums\PlatformEnum; use Database\Factories\PlatformAccountFactory; -use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Crypt; -use App\Enums\PlatformEnum; /** * @property int $id @@ -18,13 +18,14 @@ * @property string $instance_url * @property string $username * @property string $password - * @property string $settings + * @property array $settings * @property bool $is_active - * @property Carbon $last_tested_at + * @property Carbon|null $last_tested_at * @property string $status * @property Carbon $created_at * @property Carbon $updated_at * @property Collection $activeChannels + * * @method static where(string $string, PlatformEnum $platform) * @method static orderBy(string $string) * @method static create(array $validated) @@ -42,14 +43,14 @@ class PlatformAccount extends Model 'settings', 'is_active', 'last_tested_at', - 'status' + 'status', ]; protected $casts = [ 'platform' => PlatformEnum::class, 'settings' => 'array', 'is_active' => 'boolean', - 'last_tested_at' => 'datetime' + 'last_tested_at' => 'datetime', ]; // Encrypt password when storing @@ -64,12 +65,12 @@ protected function password(): Attribute if (is_null($value)) { return null; } - + // Return empty string if value is empty if (empty($value)) { return ''; } - + try { return Crypt::decryptString($value); } catch (\Exception $e) { @@ -82,18 +83,17 @@ protected function password(): Attribute if (is_null($value)) { return null; } - + // Store empty string as null if (empty($value)) { return null; } - + return Crypt::encryptString($value); }, )->withoutObjectCaching(); } - // Get the active accounts for a platform (returns collection) /** * @return Collection diff --git a/app/Models/PlatformChannel.php b/app/Models/PlatformChannel.php index 5179d0f..9291a36 100644 --- a/app/Models/PlatformChannel.php +++ b/app/Models/PlatformChannel.php @@ -10,15 +10,16 @@ /** * @method static findMany(mixed $channel_ids) - * @method static create(array $array) - * @property integer $id - * @property integer $platform_instance_id + * @method static create(array $array) + * + * @property int $id + * @property int $platform_instance_id * @property PlatformInstance $platformInstance - * @property integer $channel_id + * @property int $channel_id * @property string $name * @property int $language_id * @property Language|null $language - * @property boolean $is_active + * @property bool $is_active */ class PlatformChannel extends Model { @@ -34,11 +35,11 @@ class PlatformChannel extends Model 'channel_id', 'description', 'language_id', - 'is_active' + 'is_active', ]; protected $casts = [ - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; /** @@ -70,11 +71,11 @@ public function activePlatformAccounts(): BelongsToMany public function getFullNameAttribute(): string { // For Lemmy, use /c/ prefix - return $this->platformInstance->url . '/c/' . $this->name; + return $this->platformInstance->url.'/c/'.$this->name; } /** - * @return BelongsToMany + * @return BelongsToMany */ public function feeds(): BelongsToMany { @@ -84,7 +85,7 @@ public function feeds(): BelongsToMany } /** - * @return BelongsToMany + * @return BelongsToMany */ public function activeFeeds(): BelongsToMany { diff --git a/app/Models/PlatformChannelPost.php b/app/Models/PlatformChannelPost.php index 4dbd5b2..b2a8652 100644 --- a/app/Models/PlatformChannelPost.php +++ b/app/Models/PlatformChannelPost.php @@ -12,7 +12,9 @@ */ class PlatformChannelPost extends Model { + /** @use HasFactory<\Illuminate\Database\Eloquent\Factories\Factory> */ use HasFactory; + protected $fillable = [ 'platform', 'channel_id', @@ -44,7 +46,7 @@ public static function urlExists(PlatformEnum $platform, string $channelId, stri public static function duplicateExists(PlatformEnum $platform, string $channelId, ?string $url, ?string $title): bool { - if (!$url && !$title) { + if (! $url && ! $title) { return false; } diff --git a/app/Models/PlatformInstance.php b/app/Models/PlatformInstance.php index fa2d242..4be8a63 100644 --- a/app/Models/PlatformInstance.php +++ b/app/Models/PlatformInstance.php @@ -12,28 +12,29 @@ /** * @method static updateOrCreate(array $array, $instanceData) * @method static where(string $string, mixed $operator) + * * @property PlatformEnum $platform * @property string $url * @property string $name * @property string $description - * @property boolean $is_active + * @property bool $is_active */ class PlatformInstance extends Model { /** @use HasFactory */ use HasFactory; - + protected $fillable = [ 'platform', 'url', 'name', 'description', - 'is_active' + 'is_active', ]; protected $casts = [ 'platform' => PlatformEnum::class, - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; /** diff --git a/app/Models/Route.php b/app/Models/Route.php index b5ee7d0..1125bf5 100644 --- a/app/Models/Route.php +++ b/app/Models/Route.php @@ -4,9 +4,9 @@ use Database\Factories\RouteFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; /** @@ -21,22 +21,23 @@ class Route extends Model { /** @use HasFactory */ use HasFactory; - + protected $table = 'routes'; - + // Laravel doesn't handle composite primary keys well, so we'll use regular queries protected $primaryKey = null; + public $incrementing = false; protected $fillable = [ 'feed_id', 'platform_channel_id', 'is_active', - 'priority' + 'priority', ]; protected $casts = [ - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; /** diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 09b0381..9f1cdfb 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -2,16 +2,18 @@ namespace App\Models; +use Database\Factories\SettingFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; /** - * @method static updateOrCreate(string[] $array, array $array1) - * @method static create(string[] $array) + * @method static updateOrCreate(array $array, array $array1) + * @method static create(array $array) * @method static where(string $string, string $key) */ class Setting extends Model { + /** @use HasFactory */ use HasFactory; protected $fillable = ['key', 'value']; diff --git a/app/Models/User.php b/app/Models/User.php index a6ab88e..91135d7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,7 +11,7 @@ class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, HasApiTokens; + use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. diff --git a/app/Modules/Lemmy/LemmyRequest.php b/app/Modules/Lemmy/LemmyRequest.php index 4df170c..d64d2de 100644 --- a/app/Modules/Lemmy/LemmyRequest.php +++ b/app/Modules/Lemmy/LemmyRequest.php @@ -2,13 +2,15 @@ namespace App\Modules\Lemmy; -use Illuminate\Support\Facades\Http; use Illuminate\Http\Client\Response; +use Illuminate\Support\Facades\Http; class LemmyRequest { private string $instance; + private ?string $token; + private string $scheme = 'https'; public function __construct(string $instance, ?string $token = null) @@ -45,11 +47,12 @@ public function withScheme(string $scheme): self if (in_array($scheme, ['http', 'https'], true)) { $this->scheme = $scheme; } + return $this; } /** - * @param array $params + * @param array $params */ public function get(string $endpoint, array $params = []): Response { @@ -65,7 +68,7 @@ public function get(string $endpoint, array $params = []): Response } /** - * @param array $data + * @param array $data */ public function post(string $endpoint, array $data = []): Response { @@ -83,6 +86,7 @@ public function post(string $endpoint, array $data = []): Response public function withToken(string $token): self { $this->token = $token; + return $this; } } diff --git a/app/Modules/Lemmy/Services/LemmyApiService.php b/app/Modules/Lemmy/Services/LemmyApiService.php index 7c3bb5f..448fa57 100644 --- a/app/Modules/Lemmy/Services/LemmyApiService.php +++ b/app/Modules/Lemmy/Services/LemmyApiService.php @@ -39,7 +39,7 @@ public function login(string $username, string $password): ?string 'password' => $password, ]); - if (!$response->successful()) { + if (! $response->successful()) { $responseBody = $response->body(); logger()->error('Lemmy login failed', [ 'status' => $response->status(), @@ -61,6 +61,7 @@ public function login(string $username, string $password): ?string } $data = $response->json(); + return $data['jwt'] ?? null; } catch (Exception $e) { // Re-throw rate limit exceptions immediately @@ -74,7 +75,7 @@ public function login(string $username, string $password): ?string continue; } // Connection failed - throw exception to distinguish from auth failure - throw new Exception('Connection failed: ' . $e->getMessage()); + throw new Exception('Connection failed: '.$e->getMessage()); } } @@ -88,11 +89,12 @@ public function getCommunityId(string $communityName, string $token): int $request = new LemmyRequest($this->instance, $token); $response = $request->get('community', ['name' => $communityName]); - if (!$response->successful()) { - throw new Exception('Failed to fetch community: ' . $response->status()); + if (! $response->successful()) { + throw new Exception('Failed to fetch community: '.$response->status()); } $data = $response->json(); + return $data['community_view']['community']['id'] ?? throw new Exception('Community not found'); } catch (Exception $e) { logger()->error('Community lookup failed', ['error' => $e->getMessage()]); @@ -107,14 +109,15 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $ $response = $request->get('post/list', [ 'community_id' => $platformChannelId, 'limit' => 50, - 'sort' => 'New' + 'sort' => 'New', ]); - if (!$response->successful()) { + if (! $response->successful()) { logger()->warning('Failed to sync channel posts', [ 'status' => $response->status(), - 'platform_channel_id' => $platformChannelId + 'platform_channel_id' => $platformChannelId, ]); + return; } @@ -137,13 +140,13 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $ logger()->info('Synced channel posts', [ 'platform_channel_id' => $platformChannelId, - 'posts_count' => count($posts) + 'posts_count' => count($posts), ]); } catch (Exception $e) { logger()->error('Exception while syncing channel posts', [ 'error' => $e->getMessage(), - 'platform_channel_id' => $platformChannelId + 'platform_channel_id' => $platformChannelId, ]); } } @@ -176,8 +179,8 @@ public function createPost(string $token, string $title, string $body, int $plat $response = $request->post('post', $postData); - if (!$response->successful()) { - throw new Exception('Failed to create post: ' . $response->status() . ' - ' . $response->body()); + if (! $response->successful()) { + throw new Exception('Failed to create post: '.$response->status().' - '.$response->body()); } return $response->json(); @@ -196,19 +199,22 @@ public function getLanguages(): array $request = new LemmyRequest($this->instance); $response = $request->get('site'); - if (!$response->successful()) { + if (! $response->successful()) { logger()->warning('Failed to fetch site languages', [ - 'status' => $response->status() + 'status' => $response->status(), ]); + return []; } $data = $response->json(); + return $data['all_languages'] ?? []; } catch (Exception $e) { logger()->error('Exception while fetching languages', [ - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); + return []; } } diff --git a/app/Modules/Lemmy/Services/LemmyPublisher.php b/app/Modules/Lemmy/Services/LemmyPublisher.php index ffe60ce..3d398eb 100644 --- a/app/Modules/Lemmy/Services/LemmyPublisher.php +++ b/app/Modules/Lemmy/Services/LemmyPublisher.php @@ -12,6 +12,7 @@ class LemmyPublisher { private LemmyApiService $api; + private PlatformAccount $account; public function __construct(PlatformAccount $account) @@ -21,8 +22,9 @@ public function __construct(PlatformAccount $account) } /** - * @param array $extractedData + * @param array $extractedData * @return array + * * @throws PlatformAuthException * @throws Exception */ @@ -37,6 +39,7 @@ public function publishToChannel(Article $article, array $extractedData, Platfor // If the cached token was stale, refresh and retry once if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) { $token = $authService->refreshToken($this->account); + return $this->createPost($token, $extractedData, $channel, $article); } throw $e; @@ -44,7 +47,7 @@ public function publishToChannel(Article $article, array $extractedData, Platfor } /** - * @param array $extractedData + * @param array $extractedData * @return array */ private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article): array @@ -65,5 +68,4 @@ private function createPost(string $token, array $extractedData, PlatformChannel $languageId ); } - } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dbcab92..2a25ea8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,7 +3,9 @@ namespace App\Providers; use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Events\ExceptionOccurred; +use App\Listeners\LogActionListener; use App\Listeners\LogExceptionToDatabase; use Error; use Illuminate\Contracts\Debug\ExceptionHandler; @@ -14,12 +16,15 @@ class AppServiceProvider extends ServiceProvider { - public function register(): void - { - } + public function register(): void {} public function boot(): void { + Event::listen( + ActionPerformed::class, + LogActionListener::class, + ); + Event::listen( ExceptionOccurred::class, LogExceptionToDatabase::class, diff --git a/app/Services/Article/ArticleFetcher.php b/app/Services/Article/ArticleFetcher.php index 6669a7c..46435a8 100644 --- a/app/Services/Article/ArticleFetcher.php +++ b/app/Services/Article/ArticleFetcher.php @@ -4,9 +4,9 @@ use App\Models\Article; use App\Models\Feed; -use App\Services\Http\HttpFetcher; use App\Services\Factories\ArticleParserFactory; use App\Services\Factories\HomepageParserFactory; +use App\Services\Http\HttpFetcher; use App\Services\Log\LogSaver; use Exception; use Illuminate\Support\Collection; @@ -28,9 +28,9 @@ public function getArticlesFromFeed(Feed $feed): Collection return $this->getArticlesFromWebsiteFeed($feed); } - $this->logSaver->warning("Unsupported feed type", null, [ + $this->logSaver->warning('Unsupported feed type', null, [ 'feed_id' => $feed->id, - 'feed_type' => $feed->type + 'feed_type' => $feed->type, ]); return collect(); @@ -53,8 +53,8 @@ private function getArticlesFromRssFeed(Feed $feed): Collection libxml_use_internal_errors($previousUseErrors); } - if ($rss === false || !isset($rss->channel->item)) { - $this->logSaver->warning("Failed to parse RSS feed XML", null, [ + if ($rss === false || ! isset($rss->channel->item)) { + $this->logSaver->warning('Failed to parse RSS feed XML', null, [ 'feed_id' => $feed->id, 'feed_url' => $feed->url, ]); @@ -72,7 +72,7 @@ private function getArticlesFromRssFeed(Feed $feed): Collection return $articles; } catch (Exception $e) { - $this->logSaver->error("Failed to fetch articles from RSS feed", null, [ + $this->logSaver->error('Failed to fetch articles from RSS feed', null, [ 'feed_id' => $feed->id, 'feed_url' => $feed->url, 'error' => $e->getMessage(), @@ -92,9 +92,9 @@ private function getArticlesFromWebsiteFeed(Feed $feed): Collection $parser = HomepageParserFactory::getParserForFeed($feed); if (! $parser) { - $this->logSaver->warning("No parser available for feed URL", null, [ + $this->logSaver->warning('No parser available for feed URL', null, [ 'feed_id' => $feed->id, - 'feed_url' => $feed->url + 'feed_url' => $feed->url, ]); return collect(); @@ -107,10 +107,10 @@ private function getArticlesFromWebsiteFeed(Feed $feed): Collection ->map(fn (string $url) => $this->saveArticle($url, $feed->id)); } catch (Exception $e) { - $this->logSaver->error("Failed to fetch articles from website feed", null, [ + $this->logSaver->error('Failed to fetch articles from website feed', null, [ 'feed_id' => $feed->id, 'feed_url' => $feed->url, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); return collect(); @@ -130,7 +130,7 @@ public function fetchArticleData(Article $article): array } catch (Exception $e) { $this->logSaver->error('Exception while fetching article data', null, [ 'url' => $article->url, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); return []; @@ -156,7 +156,7 @@ private function saveArticle(string $url, ?int $feedId = null): Article return $article; } catch (\Exception $e) { - $this->logSaver->error("Failed to create article", null, [ + $this->logSaver->error('Failed to create article', null, [ 'url' => $url, 'feed_id' => $feedId, 'error' => $e->getMessage(), diff --git a/app/Services/Article/ValidationService.php b/app/Services/Article/ValidationService.php index 5a796e2..fb76392 100644 --- a/app/Services/Article/ValidationService.php +++ b/app/Services/Article/ValidationService.php @@ -12,23 +12,23 @@ public function __construct( public function validate(Article $article): Article { - logger('Checking keywords for article: ' . $article->id); + logger('Checking keywords for article: '.$article->id); $articleData = $this->articleFetcher->fetchArticleData($article); // Update article with fetched metadata (title, description) $updateData = []; - if (!empty($articleData)) { + if (! empty($articleData)) { $updateData['title'] = $articleData['title'] ?? $article->title; $updateData['description'] = $articleData['description'] ?? $article->description; $updateData['content'] = $articleData['full_article'] ?? null; } - if (!isset($articleData['full_article']) || empty($articleData['full_article'])) { + if (! isset($articleData['full_article']) || empty($articleData['full_article'])) { logger()->warning('Article data missing full_article content', [ 'article_id' => $article->id, - 'url' => $article->url + 'url' => $article->url, ]); $updateData['approval_status'] = 'rejected'; @@ -67,7 +67,7 @@ private function validateByKeywords(string $full_article): bool // Common Belgian news topics 'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy', - 'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police' + 'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police', ]; foreach ($keywords as $keyword) { diff --git a/app/Services/Auth/LemmyAuthService.php b/app/Services/Auth/LemmyAuthService.php index 7fe5f88..c599549 100644 --- a/app/Services/Auth/LemmyAuthService.php +++ b/app/Services/Auth/LemmyAuthService.php @@ -32,14 +32,14 @@ public function getToken(PlatformAccount $account): string public function refreshToken(PlatformAccount $account): string { if (! $account->username || ! $account->password || ! $account->instance_url) { - throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials for account: ' . $account->username); + throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials for account: '.$account->username); } $api = new LemmyApiService($account->instance_url); $token = $api->login($account->username, $account->password); - if (!$token) { - throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account: ' . $account->username); + if (! $token) { + throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed for account: '.$account->username); } // Cache the token for future use @@ -52,15 +52,19 @@ public function refreshToken(PlatformAccount $account): string /** * Authenticate with Lemmy API and return user data with JWT + * * @throws PlatformAuthException */ + /** + * @return array + */ public function authenticate(string $instanceUrl, string $username, string $password): array { try { $api = new LemmyApiService($instanceUrl); $token = $api->login($username, $password); - if (!$token) { + if (! $token) { // Throw a clean exception that will be caught and handled by the controller throw new PlatformAuthException(PlatformEnum::LEMMY, 'Invalid credentials'); } @@ -75,8 +79,8 @@ public function authenticate(string $instanceUrl, string $username, string $pass 'id' => 0, // Would need API call to get actual user info 'display_name' => null, 'bio' => null, - ] - ] + ], + ], ]; } catch (PlatformAuthException $e) { // Re-throw PlatformAuthExceptions as-is to avoid nesting diff --git a/app/Services/DashboardStatsService.php b/app/Services/DashboardStatsService.php index a5d8310..b1adf5a 100644 --- a/app/Services/DashboardStatsService.php +++ b/app/Services/DashboardStatsService.php @@ -9,10 +9,12 @@ use App\Models\PlatformChannel; use App\Models\Route; use Carbon\Carbon; -use Illuminate\Support\Facades\DB; class DashboardStatsService { + /** + * @return array + */ public function getStats(string $period = 'today'): array { $dateRange = $this->getDateRange($period); @@ -73,6 +75,9 @@ private function getDateRange(string $period): ?array }; } + /** + * @return array + */ public function getSystemStats(): array { $totalFeeds = Feed::query()->count(); diff --git a/app/Services/Factories/ArticleParserFactory.php b/app/Services/Factories/ArticleParserFactory.php index cfef7b3..f47d182 100644 --- a/app/Services/Factories/ArticleParserFactory.php +++ b/app/Services/Factories/ArticleParserFactory.php @@ -4,9 +4,9 @@ use App\Contracts\ArticleParserInterface; use App\Models\Feed; -use App\Services\Parsers\VrtArticleParser; use App\Services\Parsers\BelgaArticleParser; use App\Services\Parsers\GuardianArticleParser; +use App\Services\Parsers\VrtArticleParser; use Exception; class ArticleParserFactory @@ -26,7 +26,7 @@ class ArticleParserFactory public static function getParser(string $url): ArticleParserInterface { foreach (self::$parsers as $parserClass) { - $parser = new $parserClass(); + $parser = new $parserClass; if ($parser->canParse($url)) { return $parser; @@ -38,21 +38,22 @@ public static function getParser(string $url): ArticleParserInterface public static function getParserForFeed(Feed $feed, string $parserType = 'article'): ?ArticleParserInterface { - if (!$feed->provider) { + if (! $feed->provider) { return null; } $providerConfig = config("feed.providers.{$feed->provider}"); - if (!$providerConfig || !isset($providerConfig['parsers'][$parserType])) { + if (! $providerConfig || ! isset($providerConfig['parsers'][$parserType])) { return null; } + /** @var class-string $parserClass */ $parserClass = $providerConfig['parsers'][$parserType]; - if (!class_exists($parserClass)) { + if (! class_exists($parserClass)) { return null; } - return new $parserClass(); + return new $parserClass; } /** @@ -60,18 +61,19 @@ public static function getParserForFeed(Feed $feed, string $parserType = 'articl */ public static function getSupportedSources(): array { - return array_map(function($parserClass) { - $parser = new $parserClass(); + return array_map(function ($parserClass) { + $parser = new $parserClass; + return $parser->getSourceName(); }, self::$parsers); } /** - * @param class-string $parserClass + * @param class-string $parserClass */ public static function registerParser(string $parserClass): void { - if (!in_array($parserClass, self::$parsers)) { + if (! in_array($parserClass, self::$parsers)) { self::$parsers[] = $parserClass; } } diff --git a/app/Services/Factories/HomepageParserFactory.php b/app/Services/Factories/HomepageParserFactory.php index 547836e..2c244f9 100644 --- a/app/Services/Factories/HomepageParserFactory.php +++ b/app/Services/Factories/HomepageParserFactory.php @@ -9,21 +9,22 @@ class HomepageParserFactory { public static function getParserForFeed(Feed $feed): ?HomepageParserInterface { - if (!$feed->provider) { + if (! $feed->provider) { return null; } $providerConfig = config("feed.providers.{$feed->provider}"); - if (!$providerConfig || !isset($providerConfig['parsers']['homepage'])) { + if (! $providerConfig || ! isset($providerConfig['parsers']['homepage'])) { return null; } + /** @var class-string $parserClass */ $parserClass = $providerConfig['parsers']['homepage']; - if (!class_exists($parserClass)) { + if (! class_exists($parserClass)) { return null; } - $language = $feed->language?->short_code ?? 'en'; + $language = $feed->language->short_code ?? 'en'; return new $parserClass($language); } diff --git a/app/Services/Http/HttpFetcher.php b/app/Services/Http/HttpFetcher.php index 5ef5132..1caa6a6 100644 --- a/app/Services/Http/HttpFetcher.php +++ b/app/Services/Http/HttpFetcher.php @@ -2,8 +2,8 @@ namespace App\Services\Http; -use Illuminate\Support\Facades\Http; use Exception; +use Illuminate\Support\Facades\Http; class HttpFetcher { @@ -15,7 +15,7 @@ public static function fetchHtml(string $url): string try { $response = Http::get($url); - if (!$response->successful()) { + if (! $response->successful()) { throw new Exception("Failed to fetch URL: {$url} - Status: {$response->status()}"); } @@ -23,7 +23,7 @@ public static function fetchHtml(string $url): string } catch (Exception $e) { logger()->error('HTTP fetch failed', [ 'url' => $url, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); throw $e; @@ -31,7 +31,7 @@ public static function fetchHtml(string $url): string } /** - * @param array $urls + * @param array $urls * @return array> */ public static function fetchMultipleUrls(array $urls): array @@ -44,24 +44,24 @@ public static function fetchMultipleUrls(array $urls): array }); return collect($responses) - ->filter(fn($response, $index) => isset($urls[$index])) - ->reject(fn($response, $index) => $response instanceof Exception) + ->filter(fn ($response, $index) => isset($urls[$index])) + ->reject(fn ($response, $index) => $response instanceof Exception) ->map(function ($response, $index) use ($urls) { $url = $urls[$index]; - + /** @var \Illuminate\Http\Client\Response $response */ try { if ($response->successful()) { return [ 'url' => $url, 'html' => $response->body(), - 'success' => true + 'success' => true, ]; } else { return [ 'url' => $url, 'html' => null, 'success' => false, - 'status' => $response->status() + 'status' => $response->status(), ]; } } catch (Exception) { @@ -69,11 +69,10 @@ public static function fetchMultipleUrls(array $urls): array 'url' => $url, 'html' => null, 'success' => false, - 'error' => 'Exception occurred' + 'error' => 'Exception occurred', ]; } }) - ->filter(fn($result) => $result !== null) ->toArray(); } catch (Exception $e) { logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]); diff --git a/app/Services/Log/LogSaver.php b/app/Services/Log/LogSaver.php index 5a72fb4..4b3c40a 100644 --- a/app/Services/Log/LogSaver.php +++ b/app/Services/Log/LogSaver.php @@ -9,7 +9,7 @@ class LogSaver { /** - * @param array $context + * @param array $context */ public function info(string $message, ?PlatformChannel $channel = null, array $context = []): void { @@ -17,7 +17,7 @@ public function info(string $message, ?PlatformChannel $channel = null, array $c } /** - * @param array $context + * @param array $context */ public function error(string $message, ?PlatformChannel $channel = null, array $context = []): void { @@ -25,7 +25,7 @@ public function error(string $message, ?PlatformChannel $channel = null, array $ } /** - * @param array $context + * @param array $context */ public function warning(string $message, ?PlatformChannel $channel = null, array $context = []): void { @@ -33,7 +33,7 @@ public function warning(string $message, ?PlatformChannel $channel = null, array } /** - * @param array $context + * @param array $context */ public function debug(string $message, ?PlatformChannel $channel = null, array $context = []): void { @@ -41,9 +41,9 @@ public function debug(string $message, ?PlatformChannel $channel = null, array $ } /** - * @param array $context + * @param array $context */ - private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void + public function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void { $logContext = $context; diff --git a/app/Services/OnboardingService.php b/app/Services/OnboardingService.php index 2d5dbda..476f06e 100644 --- a/app/Services/OnboardingService.php +++ b/app/Services/OnboardingService.php @@ -41,6 +41,6 @@ private function checkOnboardingStatus(): bool $hasAllComponents = $hasPlatformAccount && $hasFeed && $hasChannel && $hasRoute; - return !$hasAllComponents; + return ! $hasAllComponents; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/BelgaArticlePageParser.php b/app/Services/Parsers/BelgaArticlePageParser.php index b438d32..3c1fdc4 100644 --- a/app/Services/Parsers/BelgaArticlePageParser.php +++ b/app/Services/Parsers/BelgaArticlePageParser.php @@ -10,114 +10,114 @@ public static function extractTitle(string $html): ?string if (preg_match('/]*class="[^"]*prezly-slate-heading--heading-1[^"]*"[^>]*>([^<]+)<\/h1>/i', $html, $matches)) { return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); } - + // Try meta title if (preg_match('/]*>([^<]+)<\/h1>/i', $html, $matches)) { return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); } - + // Try title tag if (preg_match('/([^<]+)<\/title>/i', $html, $matches)) { return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); } - + return null; } - + public static function extractDescription(string $html): ?string { // Try meta description first if (preg_match('/<meta property="og:description" content="([^"]+)"/i', $html, $matches)) { return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); } - + // Try Belga-specific paragraph class if (preg_match('/<p[^>]*class="[^"]*styles_paragraph__[^"]*"[^>]*>([^<]+(?:<[^\/](?!p)[^>]*>[^<]*<\/[^>]*>[^<]*)*)<\/p>/i', $html, $matches)) { return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); } - + // Try to find first paragraph in article content if (preg_match('/<p[^>]*>([^<]+(?:<[^\/](?!p)[^>]*>[^<]*<\/[^>]*>[^<]*)*)<\/p>/i', $html, $matches)) { return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); } - + return null; } - + public static function extractFullArticle(string $html): ?string { // Remove scripts, styles, and other non-content elements $cleanHtml = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', '', $html); $cleanHtml = preg_replace('/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/mi', '', $cleanHtml); - + // Look for Belga-specific paragraph class if (preg_match_all('/<p[^>]*class="[^"]*styles_paragraph__[^"]*"[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches)) { - $paragraphs = array_map(function($paragraph) { + $paragraphs = array_map(function ($paragraph) { return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); }, $matches[1]); - + // Filter out empty paragraphs and join with double newlines - $fullText = implode("\n\n", array_filter($paragraphs, function($p) { + $fullText = implode("\n\n", array_filter($paragraphs, function ($p) { return trim($p) !== ''; })); - + return $fullText ?: null; } - + // Fallback: Try to extract from prezly-slate-document section if (preg_match('/<section[^>]*class="[^"]*prezly-slate-document[^"]*"[^>]*>(.*?)<\/section>/is', $cleanHtml, $sectionMatches)) { $sectionHtml = $sectionMatches[1]; preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $sectionHtml, $matches); - - if (!empty($matches[1])) { - $paragraphs = array_map(function($paragraph) { + + if (! empty($matches[1])) { + $paragraphs = array_map(function ($paragraph) { return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); }, $matches[1]); - + // Filter out empty paragraphs and join with double newlines - $fullText = implode("\n\n", array_filter($paragraphs, function($p) { + $fullText = implode("\n\n", array_filter($paragraphs, function ($p) { return trim($p) !== ''; })); - + return $fullText ?: null; } } - + // Final fallback: Extract all paragraph content preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches); - if (!empty($matches[1])) { - $paragraphs = array_map(function($paragraph) { + if (! empty($matches[1])) { + $paragraphs = array_map(function ($paragraph) { return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); }, $matches[1]); - + // Filter out empty paragraphs and join with double newlines - $fullText = implode("\n\n", array_filter($paragraphs, function($p) { + $fullText = implode("\n\n", array_filter($paragraphs, function ($p) { return trim($p) !== ''; })); - + return $fullText ?: null; } - + return null; } - + public static function extractThumbnail(string $html): ?string { // Try OpenGraph image first if (preg_match('/<meta property="og:image" content="([^"]+)"/i', $html, $matches)) { return $matches[1]; } - + // Try first image in article content if (preg_match('/<img[^>]+src="([^"]+)"/i', $html, $matches)) { return $matches[1]; } - + return null; } @@ -133,4 +133,4 @@ public static function extractData(string $html): array 'thumbnail' => self::extractThumbnail($html), ]; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/BelgaArticleParser.php b/app/Services/Parsers/BelgaArticleParser.php index 6463290..06df134 100644 --- a/app/Services/Parsers/BelgaArticleParser.php +++ b/app/Services/Parsers/BelgaArticleParser.php @@ -20,4 +20,4 @@ public function getSourceName(): string { return 'Belga News Agency'; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/BelgaHomepageParser.php b/app/Services/Parsers/BelgaHomepageParser.php deleted file mode 100644 index 8234582..0000000 --- a/app/Services/Parsers/BelgaHomepageParser.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -namespace App\Services\Parsers; - -class BelgaHomepageParser -{ - /** - * @return array<int, string> - */ - public static function extractArticleUrls(string $html): array - { - // Find all relative article links (most articles use relative paths) - preg_match_all('/<a[^>]+href="(\/[a-z0-9-]+)"/', $html, $matches); - - // Blacklist of non-article paths - $blacklistPaths = [ - '/', - '/de', - '/feed', - '/search', - '/category', - '/about', - '/contact', - '/privacy', - '/terms', - ]; - - $urls = collect($matches[1]) - ->unique() - ->filter(function ($path) use ($blacklistPaths) { - // Exclude exact matches and paths starting with blacklisted paths - foreach ($blacklistPaths as $blacklistedPath) { - if ($path === $blacklistedPath || str_starts_with($path, $blacklistedPath . '/')) { - return false; - } - } - return true; - }) - ->map(function ($path) { - // Convert relative paths to absolute URLs - return 'https://www.belganewsagency.eu' . $path; - }) - ->values() - ->toArray(); - - return $urls; - } -} \ No newline at end of file diff --git a/app/Services/Parsers/BelgaHomepageParserAdapter.php b/app/Services/Parsers/BelgaHomepageParserAdapter.php deleted file mode 100644 index f3ba438..0000000 --- a/app/Services/Parsers/BelgaHomepageParserAdapter.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -namespace App\Services\Parsers; - -use App\Contracts\HomepageParserInterface; - -class BelgaHomepageParserAdapter implements HomepageParserInterface -{ - public function __construct( - private string $language = 'en', - ) {} - - public function canParse(string $url): bool - { - return str_contains($url, 'belganewsagency.eu'); - } - - public function extractArticleUrls(string $html): array - { - return BelgaHomepageParser::extractArticleUrls($html); - } - - public function getHomepageUrl(): string - { - return 'https://www.belganewsagency.eu/'; - } - - public function getSourceName(): string - { - return 'Belga News Agency'; - } -} \ No newline at end of file diff --git a/app/Services/Parsers/GuardianArticlePageParser.php b/app/Services/Parsers/GuardianArticlePageParser.php index 7f94570..4d46211 100644 --- a/app/Services/Parsers/GuardianArticlePageParser.php +++ b/app/Services/Parsers/GuardianArticlePageParser.php @@ -50,14 +50,14 @@ public static function extractFullArticle(string $html): ?string $sectionHtml = $sectionMatches[1]; preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $sectionHtml, $matches); - if (!empty($matches[1])) { + if (! empty($matches[1])) { return self::joinParagraphs($matches[1]); } } // Fallback: extract all paragraph content preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches); - if (!empty($matches[1])) { + if (! empty($matches[1])) { return self::joinParagraphs($matches[1]); } @@ -93,7 +93,7 @@ public static function extractData(string $html): array } /** - * @param array<int, string> $paragraphs + * @param array<int, string> $paragraphs */ private static function joinParagraphs(array $paragraphs): ?string { @@ -107,4 +107,4 @@ private static function joinParagraphs(array $paragraphs): ?string return $fullText ?: null; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/GuardianArticleParser.php b/app/Services/Parsers/GuardianArticleParser.php index a363199..edfff06 100644 --- a/app/Services/Parsers/GuardianArticleParser.php +++ b/app/Services/Parsers/GuardianArticleParser.php @@ -20,4 +20,4 @@ public function getSourceName(): string { return 'The Guardian'; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/VrtArticlePageParser.php b/app/Services/Parsers/VrtArticlePageParser.php index 323713d..e312bb6 100644 --- a/app/Services/Parsers/VrtArticlePageParser.php +++ b/app/Services/Parsers/VrtArticlePageParser.php @@ -48,13 +48,13 @@ public static function extractFullArticle(string $html): ?string // Extract all paragraph content preg_match_all('/<p[^>]*>(.*?)<\/p>/is', $cleanHtml, $matches); - if (!empty($matches[1])) { - $paragraphs = array_map(function($paragraph) { + if (! empty($matches[1])) { + $paragraphs = array_map(function ($paragraph) { return html_entity_decode(strip_tags($paragraph), ENT_QUOTES, 'UTF-8'); }, $matches[1]); // Filter out empty paragraphs and join with double newlines - $fullText = implode("\n\n", array_filter($paragraphs, function($p) { + $fullText = implode("\n\n", array_filter($paragraphs, function ($p) { return trim($p) !== ''; })); diff --git a/app/Services/Parsers/VrtArticleParser.php b/app/Services/Parsers/VrtArticleParser.php index a86199f..5e799d5 100644 --- a/app/Services/Parsers/VrtArticleParser.php +++ b/app/Services/Parsers/VrtArticleParser.php @@ -20,4 +20,4 @@ public function getSourceName(): string { return 'VRT News'; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/VrtHomepageParser.php b/app/Services/Parsers/VrtHomepageParser.php index 8aa5b17..1d62d10 100644 --- a/app/Services/Parsers/VrtHomepageParser.php +++ b/app/Services/Parsers/VrtHomepageParser.php @@ -10,13 +10,13 @@ class VrtHomepageParser public static function extractArticleUrls(string $html, string $language = 'en'): array { $escapedLanguage = preg_quote($language, '/'); - preg_match_all('/href="(?:https:\/\/www\.vrt\.be)?(\/vrtnws\/' . $escapedLanguage . '\/\d{4}\/\d{2}\/\d{2}\/[^"]+)"/', $html, $matches); + preg_match_all('/href="(?:https:\/\/www\.vrt\.be)?(\/vrtnws\/'.$escapedLanguage.'\/\d{4}\/\d{2}\/\d{2}\/[^"]+)"/', $html, $matches); $urls = collect($matches[1]) ->unique() - ->map(fn ($path) => 'https://www.vrt.be' . $path) + ->map(fn ($path) => 'https://www.vrt.be'.$path) ->toArray(); return $urls; } -} \ No newline at end of file +} diff --git a/app/Services/Parsers/VrtHomepageParserAdapter.php b/app/Services/Parsers/VrtHomepageParserAdapter.php index 557bb44..504d6ce 100644 --- a/app/Services/Parsers/VrtHomepageParserAdapter.php +++ b/app/Services/Parsers/VrtHomepageParserAdapter.php @@ -29,4 +29,4 @@ public function getSourceName(): string { return 'VRT News'; } -} \ No newline at end of file +} diff --git a/app/Services/Publishing/ArticlePublishingService.php b/app/Services/Publishing/ArticlePublishingService.php index fdb3bac..7634137 100644 --- a/app/Services/Publishing/ArticlePublishingService.php +++ b/app/Services/Publishing/ArticlePublishingService.php @@ -12,15 +12,13 @@ use App\Modules\Lemmy\Services\LemmyPublisher; use App\Services\Log\LogSaver; use Exception; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Collection; use RuntimeException; class ArticlePublishingService { - public function __construct(private LogSaver $logSaver) - { - } + public function __construct(private LogSaver $logSaver) {} + /** * Factory seam to create publisher instances (helps testing without network calls) */ @@ -28,9 +26,11 @@ protected function makePublisher(mixed $account): LemmyPublisher { return new LemmyPublisher($account); } + /** - * @param array<string, mixed> $extractedData + * @param array<string, mixed> $extractedData * @return Collection<int, ArticlePublication> + * * @throws PublishException */ public function publishToRoutedChannels(Article $article, array $extractedData): Collection @@ -60,7 +60,7 @@ public function publishToRoutedChannels(Article $article, array $extractedData): if (! $account) { $this->logSaver->warning('No active account for channel', $channel, [ 'article_id' => $article->id, - 'route_priority' => $route->priority + 'route_priority' => $route->priority, ]); return null; @@ -68,12 +68,13 @@ public function publishToRoutedChannels(Article $article, array $extractedData): return $this->publishToChannel($article, $extractedData, $channel, $account); }) - ->filter(); + ->filter(); } /** * Check if a route matches an article based on keywords - * @param array<string, mixed> $extractedData + * + * @param array<string, mixed> $extractedData */ private function routeMatchesArticle(Route $route, array $extractedData): bool { @@ -91,10 +92,10 @@ private function routeMatchesArticle(Route $route, array $extractedData): bool $articleContent = $extractedData['full_article']; } if (isset($extractedData['title'])) { - $articleContent .= ' ' . $extractedData['title']; + $articleContent .= ' '.$extractedData['title']; } if (isset($extractedData['description'])) { - $articleContent .= ' ' . $extractedData['description']; + $articleContent .= ' '.$extractedData['description']; } // Check if any of the route's keywords match the article content @@ -109,7 +110,7 @@ private function routeMatchesArticle(Route $route, array $extractedData): bool } /** - * @param array<string, mixed> $extractedData + * @param array<string, mixed> $extractedData */ private function publishToChannel(Article $article, array $extractedData, PlatformChannel $channel, mixed $account): ?ArticlePublication { @@ -145,14 +146,14 @@ private function publishToChannel(Article $article, array $extractedData, Platfo ]); $this->logSaver->info('Published to channel via keyword-filtered routing', $channel, [ - 'article_id' => $article->id + 'article_id' => $article->id, ]); return $publication; } catch (Exception $e) { $this->logSaver->warning('Failed to publish to channel', $channel, [ 'article_id' => $article->id, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); return null; diff --git a/app/Services/RoutingValidationService.php b/app/Services/RoutingValidationService.php index 7ddff2a..0ac455d 100644 --- a/app/Services/RoutingValidationService.php +++ b/app/Services/RoutingValidationService.php @@ -10,7 +10,8 @@ class RoutingValidationService { /** - * @param Collection<int, PlatformChannel> $channels + * @param Collection<int, PlatformChannel> $channels + * * @throws RoutingMismatchException */ public function validateLanguageCompatibility(Feed $feed, Collection $channels): void @@ -29,4 +30,4 @@ public function validateLanguageCompatibility(Feed $feed, Collection $channels): } } } -} \ No newline at end of file +} diff --git a/app/Services/SystemStatusService.php b/app/Services/SystemStatusService.php index 67e67bc..2909edf 100644 --- a/app/Services/SystemStatusService.php +++ b/app/Services/SystemStatusService.php @@ -3,8 +3,8 @@ namespace App\Services; use App\Models\Feed; -use App\Models\Route; use App\Models\PlatformChannel; +use App\Models\Route; use App\Models\Setting; class SystemStatusService @@ -17,22 +17,22 @@ public function getSystemStatus(): array $reasons = []; $isEnabled = true; - if (!Setting::isArticleProcessingEnabled()) { + if (! Setting::isArticleProcessingEnabled()) { $isEnabled = false; $reasons[] = 'Manually disabled by user'; } - if (!Feed::where('is_active', true)->exists()) { + if (! Feed::where('is_active', true)->exists()) { $isEnabled = false; $reasons[] = 'No active feeds configured'; } - if (!PlatformChannel::where('is_active', true)->exists()) { + if (! PlatformChannel::where('is_active', true)->exists()) { $isEnabled = false; $reasons[] = 'No active platform channels configured'; } - if (!Route::where('is_active', true)->exists()) { + if (! Route::where('is_active', true)->exists()) { $isEnabled = false; $reasons[] = 'No active feed-to-channel routes configured'; } @@ -49,4 +49,4 @@ public function canProcessArticles(): bool { return $this->getSystemStatus()['is_enabled']; } -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index ad49d34..a5feccc 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", "phpunit/phpunit": "^11.5.3" }, "autoload": { diff --git a/config/feed.php b/config/feed.php index 4ddb9ea..73227fe 100644 --- a/config/feed.php +++ b/config/feed.php @@ -33,13 +33,12 @@ 'code' => 'belga', 'name' => 'Belga News Agency', 'description' => 'Belgian national news agency', - 'type' => 'website', + 'type' => 'rss', 'is_active' => true, 'languages' => [ - 'en' => ['url' => 'https://www.belganewsagency.eu/'], + 'en' => ['url' => 'https://www.belganewsagency.eu/feed'], ], 'parsers' => [ - 'homepage' => \App\Services\Parsers\BelgaHomepageParserAdapter::class, 'article' => \App\Services\Parsers\BelgaArticleParser::class, 'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class, ], @@ -74,4 +73,4 @@ 'max_articles_per_fetch' => 50, 'article_retention_days' => 30, ], -]; \ No newline at end of file +]; diff --git a/config/languages.php b/config/languages.php index 6b63ce0..25aa231 100644 --- a/config/languages.php +++ b/config/languages.php @@ -61,4 +61,4 @@ */ 'default' => 'en', -]; \ No newline at end of file +]; diff --git a/database/factories/ArticlePublicationFactory.php b/database/factories/ArticlePublicationFactory.php index ed59ac4..4fdc624 100644 --- a/database/factories/ArticlePublicationFactory.php +++ b/database/factories/ArticlePublicationFactory.php @@ -2,8 +2,8 @@ namespace Database\Factories; -use App\Models\ArticlePublication; use App\Models\Article; +use App\Models\ArticlePublication; use App\Models\PlatformChannel; use Illuminate\Database\Eloquent\Factories\Factory; @@ -32,4 +32,4 @@ public function recentlyPublished(): static 'published_at' => $this->faker->dateTimeBetween('-1 day', 'now'), ]); } -} \ No newline at end of file +} diff --git a/database/factories/FeedFactory.php b/database/factories/FeedFactory.php index d07c3f0..fc0cb80 100644 --- a/database/factories/FeedFactory.php +++ b/database/factories/FeedFactory.php @@ -75,7 +75,8 @@ public function belga(): static { return $this->state(fn (array $attributes) => [ 'provider' => 'belga', - 'url' => 'https://www.belganewsagency.eu/', + 'url' => 'https://www.belganewsagency.eu/feed', + 'type' => 'rss', ]); } -} \ No newline at end of file +} diff --git a/database/factories/KeywordFactory.php b/database/factories/KeywordFactory.php index 57e6f0b..db36fd1 100644 --- a/database/factories/KeywordFactory.php +++ b/database/factories/KeywordFactory.php @@ -48,4 +48,4 @@ public function inactive(): static 'is_active' => false, ]); } -} \ No newline at end of file +} diff --git a/database/factories/LanguageFactory.php b/database/factories/LanguageFactory.php index 3e03213..c5ea2bf 100644 --- a/database/factories/LanguageFactory.php +++ b/database/factories/LanguageFactory.php @@ -37,4 +37,4 @@ public function english(): static 'native_name' => 'English', ]); } -} \ No newline at end of file +} diff --git a/database/factories/PlatformAccountFactory.php b/database/factories/PlatformAccountFactory.php index 6c01c17..398fe86 100644 --- a/database/factories/PlatformAccountFactory.php +++ b/database/factories/PlatformAccountFactory.php @@ -17,7 +17,7 @@ public function definition(): array { return [ 'platform' => PlatformEnum::LEMMY, - 'instance_url' => 'https://lemmy.' . $this->faker->domainName(), + 'instance_url' => 'https://lemmy.'.$this->faker->domainName(), 'username' => $this->faker->userName(), 'password' => 'test-password', 'settings' => [], @@ -49,4 +49,4 @@ public function failed(): static 'status' => 'failed', ]); } -} \ No newline at end of file +} diff --git a/database/factories/PlatformChannelFactory.php b/database/factories/PlatformChannelFactory.php index 3643da2..e087cd7 100644 --- a/database/factories/PlatformChannelFactory.php +++ b/database/factories/PlatformChannelFactory.php @@ -34,14 +34,14 @@ public function inactive(): static ]); } - public function community(string $name = null): static + public function community(?string $name = null): static { $communityName = $name ?: $this->faker->word(); - + return $this->state(fn (array $attributes) => [ 'channel_id' => strtolower($communityName), 'name' => $communityName, 'display_name' => ucfirst($communityName), ]); } -} \ No newline at end of file +} diff --git a/database/factories/PlatformInstanceFactory.php b/database/factories/PlatformInstanceFactory.php index 182f9dc..956427d 100644 --- a/database/factories/PlatformInstanceFactory.php +++ b/database/factories/PlatformInstanceFactory.php @@ -33,8 +33,8 @@ public function lemmy(): static { return $this->state(fn (array $attributes) => [ 'platform' => 'lemmy', - 'name' => 'Lemmy ' . $this->faker->word(), - 'url' => 'https://lemmy.' . $this->faker->domainName(), + 'name' => 'Lemmy '.$this->faker->word(), + 'url' => 'https://lemmy.'.$this->faker->domainName(), ]); } -} \ No newline at end of file +} diff --git a/database/factories/RouteFactory.php b/database/factories/RouteFactory.php index 53177ac..93684f0 100644 --- a/database/factories/RouteFactory.php +++ b/database/factories/RouteFactory.php @@ -2,9 +2,9 @@ namespace Database\Factories; -use App\Models\Route; use App\Models\Feed; use App\Models\PlatformChannel; +use App\Models\Route; use Illuminate\Database\Eloquent\Factories\Factory; class RouteFactory extends Factory @@ -36,4 +36,4 @@ public function inactive(): static 'is_active' => false, ]); } -} \ No newline at end of file +} diff --git a/database/factories/SettingFactory.php b/database/factories/SettingFactory.php index a8aa2a6..9ac8fe0 100644 --- a/database/factories/SettingFactory.php +++ b/database/factories/SettingFactory.php @@ -32,4 +32,4 @@ public function withValue(string $value): static 'value' => $value, ]); } -} \ No newline at end of file +} diff --git a/database/migrations/2024_01_01_000002_create_languages.php b/database/migrations/2024_01_01_000002_create_languages.php index 0b55878..4e52a68 100644 --- a/database/migrations/2024_01_01_000002_create_languages.php +++ b/database/migrations/2024_01_01_000002_create_languages.php @@ -23,4 +23,4 @@ public function down(): void { Schema::dropIfExists('languages'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2024_01_01_000004_create_feeds_and_routes.php b/database/migrations/2024_01_01_000004_create_feeds_and_routes.php index 1bf820a..15099d8 100644 --- a/database/migrations/2024_01_01_000004_create_feeds_and_routes.php +++ b/database/migrations/2024_01_01_000004_create_feeds_and_routes.php @@ -60,4 +60,4 @@ public function down(): void Schema::dropIfExists('routes'); Schema::dropIfExists('feeds'); } -}; \ No newline at end of file +}; diff --git a/database/seeders/LanguageSeeder.php b/database/seeders/LanguageSeeder.php index 389d452..b967d88 100644 --- a/database/seeders/LanguageSeeder.php +++ b/database/seeders/LanguageSeeder.php @@ -21,4 +21,4 @@ public function run(): void ); } } -} \ No newline at end of file +} diff --git a/database/seeders/PlatformInstanceSeeder.php b/database/seeders/PlatformInstanceSeeder.php index 1b3e841..1d47848 100644 --- a/database/seeders/PlatformInstanceSeeder.php +++ b/database/seeders/PlatformInstanceSeeder.php @@ -17,14 +17,13 @@ public function run(): void 'name' => 'Belgae Social', 'description' => 'A Belgian Lemmy instance on the fediverse', ], - ])->each (fn ($instanceData) => - PlatformInstance::updateOrCreate( - [ - 'platform' => $instanceData['platform'], - 'url' => $instanceData['url'], - ], - $instanceData - ) + ])->each(fn ($instanceData) => PlatformInstance::updateOrCreate( + [ + 'platform' => $instanceData['platform'], + 'url' => $instanceData['url'], + ], + $instanceData + ) ); } -} \ No newline at end of file +} diff --git a/database/seeders/SettingsSeeder.php b/database/seeders/SettingsSeeder.php index 8f6b388..115153d 100644 --- a/database/seeders/SettingsSeeder.php +++ b/database/seeders/SettingsSeeder.php @@ -3,7 +3,6 @@ namespace Database\Seeders; use App\Models\Setting; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class SettingsSeeder extends Seeder diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..fb3f0fd --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,109 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined static method App\\Models\\Feed\:\:withTrashed\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: tests/Feature/DatabaseIntegrationTest.php + + - + message: '#^Access to an undefined property App\\Models\\PlatformAccount\:\:\$pivot\.$#' + identifier: property.notFound + count: 1 + path: tests/Unit/Actions/CreateChannelActionTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with int will always evaluate to false\.$#' + identifier: method.impossibleType + count: 1 + path: tests/Unit/Actions/CreateChannelActionTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with string will always evaluate to false\.$#' + identifier: method.impossibleType + count: 2 + path: tests/Unit/Actions/CreateFeedActionTest.php + + - + message: '#^Access to an undefined property App\\Models\\Route\:\:\$id\.$#' + identifier: property.notFound + count: 2 + path: tests/Unit/Actions/CreateRouteActionTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertFalse\(\) with false will always evaluate to false\.$#' + identifier: method.impossibleType + count: 1 + path: tests/Unit/Enums/LogLevelEnumTest.php + + - + message: '#^Strict comparison using \=\=\= between App\\Enums\\LogLevelEnum\:\:DEBUG and App\\Enums\\LogLevelEnum\:\:DEBUG will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/Unit/Enums/LogLevelEnumTest.php + + - + message: '#^Strict comparison using \=\=\= between App\\Enums\\LogLevelEnum\:\:DEBUG and App\\Enums\\LogLevelEnum\:\:INFO will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: tests/Unit/Enums/LogLevelEnumTest.php + + - + message: '#^Strict comparison using \=\=\= between App\\Enums\\PlatformEnum\:\:LEMMY and App\\Enums\\PlatformEnum\:\:LEMMY will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/Unit/Enums/PlatformEnumTest.php + + - + message: '#^Access to an undefined property App\\Models\\PlatformChannel\:\:\$pivot\.$#' + identifier: property.notFound + count: 2 + path: tests/Unit/Models/FeedTest.php + + - + message: '#^Access to an undefined property App\\Models\\PlatformChannel\:\:\$pivot\.$#' + identifier: property.notFound + count: 6 + path: tests/Unit/Models/PlatformAccountTest.php + + - + message: '#^Access to an undefined property App\\Models\\Feed\:\:\$pivot\.$#' + identifier: property.notFound + count: 2 + path: tests/Unit/Models/PlatformChannelTest.php + + - + message: '#^Access to an undefined property App\\Models\\PlatformAccount\:\:\$pivot\.$#' + identifier: property.notFound + count: 6 + path: tests/Unit/Models/PlatformChannelTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with int will always evaluate to false\.$#' + identifier: method.impossibleType + count: 1 + path: tests/Unit/Models/PlatformChannelTest.php + + - + message: '#^Access to an undefined property App\\Models\\Language\:\:\$pivot\.$#' + identifier: property.notFound + count: 7 + path: tests/Unit/Models/PlatformInstanceTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with string will always evaluate to false\.$#' + identifier: method.impossibleType + count: 1 + path: tests/Unit/Models/PlatformInstanceTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with string will always evaluate to false\.$#' + identifier: method.impossibleType + count: 1 + path: tests/Unit/Models/RouteTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with int will always evaluate to false\.$#' + identifier: method.impossibleType + count: 1 + path: tests/Unit/Services/ArticleFetcherTest.php diff --git a/phpstan.neon b/phpstan.neon index ce555e2..eb281c6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,7 @@ includes: - vendor/larastan/larastan/extension.neon + - vendor/phpstan/phpstan-mockery/extension.neon + - phpstan-baseline.neon parameters: level: 7 @@ -10,3 +12,7 @@ parameters: excludePaths: - bootstrap/*.php - storage/* + + ignoreErrors: + - identifier: method.alreadyNarrowedType + - identifier: function.alreadyNarrowedType diff --git a/resources/views/livewire/routes.blade.php b/resources/views/livewire/routes.blade.php index 364bbfa..63ef1ec 100644 --- a/resources/views/livewire/routes.blade.php +++ b/resources/views/livewire/routes.blade.php @@ -47,7 +47,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font <span>•</span> <span>Feed: {{ $route->feed?->name }}</span> <span>•</span> - <span>Channel: {{ $route->platformChannel?->display_name ?? $route->platformChannel?->name }}</span> + <span>{{ $route->platformChannel?->platformInstance?->platform?->channelLabel() ?? 'Channel' }}: {{ $route->platformChannel?->display_name ?? $route->platformChannel?->name }}</span> <span>•</span> <span>Created: {{ $route->created_at->format('M d, Y') }}</span> </div> @@ -246,7 +246,7 @@ class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transp <strong>Feed:</strong> {{ $editingRoute->feed?->name }} </p> <p class="text-sm text-gray-600"> - <strong>Channel:</strong> {{ $editingRoute->platformChannel?->display_name ?? $editingRoute->platformChannel?->name }} + <strong>{{ $editingRoute->platformChannel?->platformInstance?->platform?->channelLabel() ?? 'Channel' }}:</strong> {{ $editingRoute->platformChannel?->display_name ?? $editingRoute->platformChannel?->name }} </p> </div> diff --git a/routes/api.php b/routes/api.php index 3142d04..7e12a04 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,12 +4,12 @@ use App\Http\Controllers\Api\V1\AuthController; use App\Http\Controllers\Api\V1\DashboardController; use App\Http\Controllers\Api\V1\FeedsController; +use App\Http\Controllers\Api\V1\KeywordsController; use App\Http\Controllers\Api\V1\LogsController; use App\Http\Controllers\Api\V1\OnboardingController; use App\Http\Controllers\Api\V1\PlatformAccountsController; use App\Http\Controllers\Api\V1\PlatformChannelsController; use App\Http\Controllers\Api\V1\RoutingController; -use App\Http\Controllers\Api\V1\KeywordsController; use App\Http\Controllers\Api\V1\SettingsController; use Illuminate\Support\Facades\Route; diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php index d0c808b..cc68301 100644 --- a/tests/CreatesApplication.php +++ b/tests/CreatesApplication.php @@ -18,4 +18,4 @@ public function createApplication(): Application return $app; } -} \ No newline at end of file +} diff --git a/tests/Feature/ApiAccessTest.php b/tests/Feature/ApiAccessTest.php index 1ebb64a..d4f37cb 100644 --- a/tests/Feature/ApiAccessTest.php +++ b/tests/Feature/ApiAccessTest.php @@ -2,11 +2,6 @@ namespace Tests\Feature; -use App\Models\Article; -use App\Models\Feed; -use App\Models\PlatformAccount; -use App\Models\PlatformChannel; -use App\Models\Setting; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -31,7 +26,7 @@ public function test_api_routes_are_publicly_accessible(): void '/api/v1/feeds', '/api/v1/routing', '/api/v1/settings', - '/api/v1/logs' + '/api/v1/logs', ]; foreach ($routes as $route) { @@ -49,7 +44,7 @@ public function test_fallback_route_returns_api_message(): void $response->assertStatus(404); $response->assertJson([ 'message' => 'This is the FFR API backend. Use /api/v1/* endpoints or check the React frontend.', - 'api_base' => '/api/v1' + 'api_base' => '/api/v1', ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/DatabaseIntegrationTest.php b/tests/Feature/DatabaseIntegrationTest.php index e978048..e7c9a1d 100644 --- a/tests/Feature/DatabaseIntegrationTest.php +++ b/tests/Feature/DatabaseIntegrationTest.php @@ -27,12 +27,12 @@ public function test_user_model_creates_successfully(): void { $user = User::factory()->create([ 'name' => 'Test User', - 'email' => 'test@example.com' + 'email' => 'test@example.com', ]); $this->assertDatabaseHas('users', [ 'name' => 'Test User', - 'email' => 'test@example.com' + 'email' => 'test@example.com', ]); $this->assertEquals('Test User', $user->name); @@ -43,38 +43,40 @@ public function test_language_model_creates_successfully(): void { $language = Language::factory()->create([ 'name' => 'English', - 'short_code' => 'en' + 'short_code' => 'en', ]); $this->assertDatabaseHas('languages', [ 'name' => 'English', - 'short_code' => 'en' + 'short_code' => 'en', ]); } public function test_platform_instance_model_creates_successfully(): void { + /** @var PlatformInstance $instance */ $instance = PlatformInstance::factory()->create([ 'name' => 'Test Instance', - 'url' => 'https://test.lemmy.world' + 'url' => 'https://test.lemmy.world', ]); $this->assertDatabaseHas('platform_instances', [ 'name' => 'Test Instance', - 'url' => 'https://test.lemmy.world' + 'url' => 'https://test.lemmy.world', ]); } public function test_platform_account_model_creates_successfully(): void { + /** @var PlatformAccount $account */ $account = PlatformAccount::factory()->create([ 'username' => 'testuser', - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('platform_accounts', [ 'username' => 'testuser', - 'is_active' => true + 'is_active' => true, ]); $this->assertEquals('testuser', $account->username); @@ -84,19 +86,19 @@ public function test_platform_channel_model_creates_successfully(): void { $language = Language::factory()->create(); $instance = PlatformInstance::factory()->create(); - + $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, 'language_id' => $language->id, 'name' => 'Test Channel', - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('platform_channels', [ 'platform_instance_id' => $instance->id, 'language_id' => $language->id, 'name' => 'Test Channel', - 'is_active' => true + 'is_active' => true, ]); $this->assertEquals($instance->id, $channel->platformInstance->id); @@ -106,19 +108,19 @@ public function test_platform_channel_model_creates_successfully(): void public function test_feed_model_creates_successfully(): void { $language = Language::factory()->create(); - + $feed = Feed::factory()->create([ 'language_id' => $language->id, 'name' => 'Test Feed', 'url' => 'https://example.com/feed.rss', - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('feeds', [ 'language_id' => $language->id, 'name' => 'Test Feed', 'url' => 'https://example.com/feed.rss', - 'is_active' => true + 'is_active' => true, ]); $this->assertEquals($language->id, $feed->language->id); @@ -127,19 +129,19 @@ public function test_feed_model_creates_successfully(): void public function test_article_model_creates_successfully(): void { $feed = Feed::factory()->create(); - + $article = Article::factory()->create([ 'feed_id' => $feed->id, 'title' => 'Test Article', 'url' => 'https://example.com/article', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $this->assertDatabaseHas('articles', [ 'feed_id' => $feed->id, 'title' => 'Test Article', 'url' => 'https://example.com/article', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $this->assertEquals($feed->id, $article->feed->id); @@ -149,20 +151,20 @@ public function test_article_publication_model_creates_successfully(): void { $article = Article::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $publication = ArticlePublication::create([ 'article_id' => $article->id, 'platform_channel_id' => $channel->id, 'post_id' => 'test-post-123', 'published_at' => now(), - 'published_by' => 'test-user' + 'published_by' => 'test-user', ]); $this->assertDatabaseHas('article_publications', [ 'article_id' => $article->id, 'platform_channel_id' => $channel->id, 'post_id' => 'test-post-123', - 'published_by' => 'test-user' + 'published_by' => 'test-user', ]); $this->assertEquals($article->id, $publication->article->id); @@ -173,17 +175,18 @@ public function test_route_model_creates_successfully(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + + /** @var Route $route */ $route = Route::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('routes', [ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'is_active' => true + 'is_active' => true, ]); $this->assertEquals($feed->id, $route->feed->id); @@ -196,16 +199,16 @@ public function test_platform_channel_post_model_creates_successfully(): void // Likely due to test pollution that's difficult to isolate // Commenting out for now since the model works correctly $this->assertTrue(true); - + // $post = new PlatformChannelPost([ // 'platform' => PlatformEnum::LEMMY, - // 'channel_id' => 'technology', + // 'channel_id' => 'technology', // 'post_id' => 'external-post-123', // 'title' => 'Test Post', // 'url' => 'https://example.com/post', // 'posted_at' => now() // ]); - + // $post->save(); // $this->assertDatabaseHas('platform_channel_posts', [ @@ -222,20 +225,20 @@ public function test_keyword_model_creates_successfully(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $keyword = Keyword::factory() ->forFeed($feed) ->forChannel($channel) ->create([ 'keyword' => 'test keyword', - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('keywords', [ 'keyword' => 'test keyword', 'is_active' => true, 'feed_id' => $feed->id, - 'platform_channel_id' => $channel->id + 'platform_channel_id' => $channel->id, ]); } @@ -245,12 +248,12 @@ public function test_log_model_creates_successfully(): void 'level' => 'info', 'message' => 'Test log message', 'context' => json_encode(['key' => 'value']), - 'logged_at' => now() + 'logged_at' => now(), ]); $this->assertDatabaseHas('logs', [ 'level' => 'info', - 'message' => 'Test log message' + 'message' => 'Test log message', ]); } @@ -258,12 +261,12 @@ public function test_setting_model_creates_successfully(): void { $setting = Setting::create([ 'key' => 'test_setting', - 'value' => 'test_value' + 'value' => 'test_value', ]); $this->assertDatabaseHas('settings', [ 'key' => 'test_setting', - 'value' => 'test_value' + 'value' => 'test_value', ]); } @@ -273,7 +276,7 @@ public function test_feed_articles_relationship(): void $articles = Article::factory()->count(3)->create(['feed_id' => $feed->id]); $this->assertCount(3, $feed->articles); - + foreach ($articles as $article) { $this->assertTrue($feed->articles->contains($article)); } @@ -283,14 +286,14 @@ public function test_platform_account_channels_many_to_many_relationship(): void { $account = PlatformAccount::factory()->create(); $channel = PlatformChannel::factory()->create(); - + // Test the pivot table relationship $account->channels()->attach($channel->id, ['is_active' => true, 'priority' => 1]); - + $this->assertDatabaseHas('platform_account_channels', [ 'platform_account_id' => $account->id, 'platform_channel_id' => $channel->id, - 'is_active' => true + 'is_active' => true, ]); } @@ -298,14 +301,14 @@ public function test_language_platform_instances_relationship(): void { $language = Language::factory()->create(); $instances = PlatformInstance::factory()->count(2)->create(); - + // Attach language to instances via pivot table foreach ($instances as $instance) { $language->platformInstances()->attach($instance->id, ['platform_language_id' => rand(1, 100)]); } $this->assertCount(2, $language->platformInstances); - + foreach ($instances as $instance) { $this->assertTrue($language->platformInstances->contains($instance)); } @@ -316,12 +319,12 @@ public function test_model_soft_deletes_work_correctly(): void // Test models that might use soft deletes $feed = Feed::factory()->create(); $feedId = $feed->id; - + $feed->delete(); - + // Should not find with normal query if soft deleted $this->assertNull(Feed::find($feedId)); - + // Should find with withTrashed if model uses soft deletes if (method_exists($feed, 'withTrashed')) { $this->assertNotNull(Feed::withTrashed()->find($feedId)); @@ -332,7 +335,7 @@ public function test_database_constraints_are_enforced(): void { // Test foreign key constraints $this->expectException(\Illuminate\Database\QueryException::class); - + // Try to create article with non-existent feed_id Article::factory()->create(['feed_id' => 99999]); } @@ -357,4 +360,4 @@ public function test_all_factories_work_correctly(): void $this->assertInstanceOf(\Illuminate\Database\Eloquent\Model::class, $model); } } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php b/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php index a74e36b..0785665 100644 --- a/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php +++ b/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php @@ -78,7 +78,7 @@ public function test_command_skips_when_article_processing_disabled(): void Queue::fake(); Setting::create([ 'key' => 'article_processing_enabled', - 'value' => '0' + 'value' => '0', ]); // Act diff --git a/tests/Feature/Http/Console/Commands/SyncChannelPostsCommandTest.php b/tests/Feature/Http/Console/Commands/SyncChannelPostsCommandTest.php index dc1b71f..0bb8115 100644 --- a/tests/Feature/Http/Console/Commands/SyncChannelPostsCommandTest.php +++ b/tests/Feature/Http/Console/Commands/SyncChannelPostsCommandTest.php @@ -2,9 +2,7 @@ namespace Tests\Feature\Http\Console\Commands; -use App\Jobs\SyncChannelPostsJob; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Queue; use Illuminate\Testing\PendingCommand; use Tests\TestCase; @@ -36,8 +34,9 @@ public function test_command_returns_failure_exit_code_for_unsupported_platform( public function test_command_accepts_lemmy_platform_argument(): void { // Act - Test that the command accepts lemmy as a valid platform argument + /** @var PendingCommand $exitCode */ $exitCode = $this->artisan('channel:sync lemmy'); - + // Assert - Command should succeed (not fail with argument validation error) $exitCode->assertSuccessful(); $exitCode->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels'); @@ -46,10 +45,11 @@ public function test_command_accepts_lemmy_platform_argument(): void public function test_command_handles_default_platform(): void { // Act - Test that the command works with default platform (should be lemmy) + /** @var PendingCommand $exitCode */ $exitCode = $this->artisan('channel:sync'); - + // Assert - Command should succeed with default platform $exitCode->assertSuccessful(); $exitCode->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels'); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/ArticlesControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/ArticlesControllerTest.php index 0c5c8d2..9b16f24 100644 --- a/tests/Feature/Http/Controllers/Api/V1/ArticlesControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/ArticlesControllerTest.php @@ -4,7 +4,6 @@ use App\Models\Article; use App\Models\Feed; -use App\Models\Setting; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -33,7 +32,7 @@ public function test_index_returns_successful_response(): void 'publishing_approvals_enabled', ], ], - 'message' + 'message', ]); } @@ -53,7 +52,7 @@ public function test_index_returns_articles_with_pagination(): void 'total' => 25, 'last_page' => 3, ], - ] + ], ]); $this->assertCount(10, $response->json('data.articles')); @@ -74,30 +73,30 @@ public function test_index_respects_per_page_limit(): void 'pagination' => [ 'per_page' => 100, // Should be capped at 100 ], - ] + ], ]); } public function test_index_orders_articles_by_created_at_desc(): void { $feed = Feed::factory()->create(); - + $firstArticle = Article::factory()->create([ 'feed_id' => $feed->id, 'created_at' => now()->subHours(2), - 'title' => 'First Article' + 'title' => 'First Article', ]); - + $secondArticle = Article::factory()->create([ 'feed_id' => $feed->id, 'created_at' => now()->subHour(), - 'title' => 'Second Article' + 'title' => 'Second Article', ]); $response = $this->getJson('/api/v1/articles'); $response->assertStatus(200); - + $articles = $response->json('data.articles'); $this->assertEquals('Second Article', $articles[0]['title']); $this->assertEquals('First Article', $articles[1]['title']); @@ -108,7 +107,7 @@ public function test_approve_article_successfully(): void $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $response = $this->postJson("/api/v1/articles/{$article->id}/approve"); @@ -116,7 +115,7 @@ public function test_approve_article_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Article approved and queued for publishing.' + 'message' => 'Article approved and queued for publishing.', ]); $article->refresh(); @@ -135,7 +134,7 @@ public function test_reject_article_successfully(): void $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $response = $this->postJson("/api/v1/articles/{$article->id}/reject"); @@ -143,7 +142,7 @@ public function test_reject_article_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Article rejected.' + 'message' => 'Article rejected.', ]); $article->refresh(); @@ -165,9 +164,9 @@ public function test_index_includes_settings(): void ->assertJsonStructure([ 'data' => [ 'settings' => [ - 'publishing_approvals_enabled' - ] - ] + 'publishing_approvals_enabled', + ], + ], ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/DashboardControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/DashboardControllerTest.php index 6dea95f..6a3ed63 100644 --- a/tests/Feature/Http/Controllers/Api/V1/DashboardControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/DashboardControllerTest.php @@ -38,7 +38,7 @@ public function test_stats_returns_successful_response(): void 'available_periods', 'current_period', ], - 'message' + 'message', ]); } @@ -54,7 +54,7 @@ public function test_stats_with_different_periods(): void 'success' => true, 'data' => [ 'current_period' => $period, - ] + ], ]); } } @@ -80,7 +80,7 @@ public function test_stats_with_sample_data(): void ArticlePublication::factory()->create([ 'article_id' => $articles->first()->id, 'platform_channel_id' => $channel->id, - 'published_at' => now() + 'published_at' => now(), ]); $response = $this->getJson('/api/v1/dashboard/stats?period=all'); @@ -93,7 +93,7 @@ public function test_stats_with_sample_data(): void 'articles_fetched' => $initialArticles + 3, 'articles_published' => $initialPublications + 1, ], - ] + ], ]); // Just verify structure and that we have more items than we started with @@ -126,7 +126,7 @@ public function test_stats_returns_empty_data_with_no_records(): void 'total_routes' => 0, 'active_routes' => 0, ], - ] + ], ]); } } diff --git a/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php index 143a8f5..0acac2b 100644 --- a/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php @@ -20,9 +20,9 @@ public function test_index_returns_successful_response(): void 'success', 'data' => [ 'feeds', - 'pagination' + 'pagination', ], - 'message' + 'message', ]); } @@ -36,7 +36,7 @@ public function test_index_returns_feeds_ordered_by_active_status_then_name(): v $response->assertStatus(200); $feeds = $response->json('data.feeds'); - + // First should be active feeds in alphabetical order $this->assertEquals('A Feed', $feeds[0]['name']); $this->assertTrue($feeds[0]['is_active']); @@ -69,7 +69,7 @@ public function test_store_creates_vrt_feed_successfully(): void 'url' => 'https://www.vrt.be/vrtnws/en/', 'type' => 'website', 'is_active' => true, - ] + ], ]); $this->assertDatabaseHas('feeds', [ @@ -98,16 +98,16 @@ public function test_store_creates_belga_feed_successfully(): void 'message' => 'Feed created successfully!', 'data' => [ 'name' => 'Belga Test Feed', - 'url' => 'https://www.belganewsagency.eu/', - 'type' => 'website', + 'url' => 'https://www.belganewsagency.eu/feed', + 'type' => 'rss', 'is_active' => true, - ] + ], ]); $this->assertDatabaseHas('feeds', [ 'name' => 'Belga Test Feed', - 'url' => 'https://www.belganewsagency.eu/', - 'type' => 'website', + 'url' => 'https://www.belganewsagency.eu/feed', + 'type' => 'rss', ]); } @@ -133,7 +133,7 @@ public function test_store_creates_guardian_feed_successfully(): void 'url' => 'https://www.theguardian.com/international/rss', 'type' => 'rss', 'is_active' => true, - ] + ], ]); $this->assertDatabaseHas('feeds', [ @@ -160,7 +160,7 @@ public function test_store_sets_default_active_status(): void ->assertJson([ 'data' => [ 'is_active' => true, // Should default to true - ] + ], ]); } @@ -175,7 +175,7 @@ public function test_store_validates_required_fields(): void public function test_store_rejects_invalid_provider(): void { $language = Language::factory()->create(); - + $feedData = [ 'name' => 'Invalid Feed', 'provider' => 'invalid', @@ -201,7 +201,7 @@ public function test_show_returns_feed_successfully(): void 'data' => [ 'id' => $feed->id, 'name' => $feed->name, - ] + ], ]); } @@ -216,7 +216,7 @@ public function test_update_modifies_feed_successfully(): void { $language = Language::factory()->create(); $feed = Feed::factory()->language($language)->create(['name' => 'Original Name']); - + $updateData = [ 'name' => 'Updated Name', 'url' => $feed->url, @@ -232,7 +232,7 @@ public function test_update_modifies_feed_successfully(): void 'message' => 'Feed updated successfully!', 'data' => [ 'name' => 'Updated Name', - ] + ], ]); $this->assertDatabaseHas('feeds', [ @@ -245,7 +245,7 @@ public function test_update_preserves_active_status_when_not_provided(): void { $language = Language::factory()->create(); $feed = Feed::factory()->language($language)->create(['is_active' => false]); - + $updateData = [ 'name' => $feed->name, 'url' => $feed->url, @@ -260,7 +260,7 @@ public function test_update_preserves_active_status_when_not_provided(): void ->assertJson([ 'data' => [ 'is_active' => false, // Should preserve original value - ] + ], ]); } @@ -298,7 +298,7 @@ public function test_toggle_activates_inactive_feed(): void 'message' => 'Feed activated successfully!', 'data' => [ 'is_active' => true, - ] + ], ]); $this->assertDatabaseHas('feeds', [ @@ -319,7 +319,7 @@ public function test_toggle_deactivates_active_feed(): void 'message' => 'Feed deactivated successfully!', 'data' => [ 'is_active' => false, - ] + ], ]); $this->assertDatabaseHas('feeds', [ @@ -334,4 +334,4 @@ public function test_toggle_returns_404_for_nonexistent_feed(): void $response->assertStatus(404); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/KeywordsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/KeywordsControllerTest.php index 3934a15..f3b749a 100644 --- a/tests/Feature/Http/Controllers/Api/V1/KeywordsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/KeywordsControllerTest.php @@ -14,21 +14,23 @@ class KeywordsControllerTest extends TestCase use RefreshDatabase; protected Feed $feed; + protected PlatformChannel $channel; + protected Route $route; protected function setUp(): void { parent::setUp(); - + $this->feed = Feed::factory()->create(); $this->channel = PlatformChannel::factory()->create(); - + $this->route = Route::create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); } @@ -38,7 +40,7 @@ public function test_can_get_keywords_for_route(): void 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel->id, 'keyword' => 'test keyword', - 'is_active' => true + 'is_active' => true, ]); $response = $this->getJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords"); @@ -52,9 +54,9 @@ public function test_can_get_keywords_for_route(): void 'keyword', 'is_active', 'feed_id', - 'platform_channel_id' - ] - ] + 'platform_channel_id', + ], + ], ]) ->assertJsonPath('data.0.keyword', 'test keyword'); } @@ -63,7 +65,7 @@ public function test_can_create_keyword_for_route(): void { $keywordData = [ 'keyword' => 'new keyword', - 'is_active' => true + 'is_active' => true, ]; $response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", $keywordData); @@ -76,8 +78,8 @@ public function test_can_create_keyword_for_route(): void 'keyword', 'is_active', 'feed_id', - 'platform_channel_id' - ] + 'platform_channel_id', + ], ]) ->assertJsonPath('data.keyword', 'new keyword') ->assertJsonPath('data.is_active', true); @@ -86,7 +88,7 @@ public function test_can_create_keyword_for_route(): void 'keyword' => 'new keyword', 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel->id, - 'is_active' => true + 'is_active' => true, ]); } @@ -95,11 +97,11 @@ public function test_cannot_create_duplicate_keyword_for_route(): void Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel->id, - 'keyword' => 'duplicate keyword' + 'keyword' => 'duplicate keyword', ]); $response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", [ - 'keyword' => 'duplicate keyword' + 'keyword' => 'duplicate keyword', ]); $response->assertStatus(409) @@ -109,14 +111,15 @@ public function test_cannot_create_duplicate_keyword_for_route(): void public function test_can_update_keyword(): void { + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel->id, - 'is_active' => true + 'is_active' => true, ]); $response = $this->putJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}", [ - 'is_active' => false + 'is_active' => false, ]); $response->assertStatus(200) @@ -124,15 +127,16 @@ public function test_can_update_keyword(): void $this->assertDatabaseHas('keywords', [ 'id' => $keyword->id, - 'is_active' => false + 'is_active' => false, ]); } public function test_can_delete_keyword(): void { + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create([ 'feed_id' => $this->feed->id, - 'platform_channel_id' => $this->channel->id + 'platform_channel_id' => $this->channel->id, ]); $response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}"); @@ -140,16 +144,17 @@ public function test_can_delete_keyword(): void $response->assertStatus(200); $this->assertDatabaseMissing('keywords', [ - 'id' => $keyword->id + 'id' => $keyword->id, ]); } public function test_can_toggle_keyword(): void { + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel->id, - 'is_active' => true + 'is_active' => true, ]); $response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}/toggle"); @@ -159,7 +164,7 @@ public function test_can_toggle_keyword(): void $this->assertDatabaseHas('keywords', [ 'id' => $keyword->id, - 'is_active' => false + 'is_active' => false, ]); } @@ -167,10 +172,11 @@ public function test_cannot_access_keyword_from_different_route(): void { $otherFeed = Feed::factory()->create(); $otherChannel = PlatformChannel::factory()->create(); - + + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create([ 'feed_id' => $otherFeed->id, - 'platform_channel_id' => $otherChannel->id + 'platform_channel_id' => $otherChannel->id, ]); $response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}"); @@ -186,4 +192,4 @@ public function test_validates_required_fields(): void $response->assertStatus(422) ->assertJsonValidationErrors(['keyword']); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/LogsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/LogsControllerTest.php index de1a42b..71c0288 100644 --- a/tests/Feature/Http/Controllers/Api/V1/LogsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/LogsControllerTest.php @@ -37,7 +37,7 @@ public function test_index_returns_successful_response(): void 'context', 'created_at', 'updated_at', - ] + ], ], 'pagination' => [ 'current_page', @@ -46,12 +46,12 @@ public function test_index_returns_successful_response(): void 'total', 'from', 'to', - ] - ] + ], + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Logs retrieved successfully.' + 'message' => 'Logs retrieved successfully.', ]); } @@ -64,9 +64,9 @@ public function test_index_orders_logs_by_created_at_desc(): void $response = $this->getJson('/api/v1/logs'); $response->assertStatus(200); - + $logs = $response->json('data.logs'); - + $this->assertEquals($newestLog->id, $logs[0]['id']); $this->assertEquals($newLog->id, $logs[1]['id']); $this->assertEquals($oldLog->id, $logs[2]['id']); @@ -81,9 +81,9 @@ public function test_index_filters_by_level(): void $response = $this->getJson('/api/v1/logs?level=error'); $response->assertStatus(200); - + $logs = $response->json('data.logs'); - + $this->assertCount(1, $logs); $this->assertEquals('error', $logs[0]['level']); } @@ -95,10 +95,10 @@ public function test_index_respects_per_page_parameter(): void $response = $this->getJson('/api/v1/logs?per_page=5'); $response->assertStatus(200); - + $logs = $response->json('data.logs'); $pagination = $response->json('data.pagination'); - + $this->assertCount(5, $logs); $this->assertEquals(5, $pagination['per_page']); $this->assertEquals(15, $pagination['total']); @@ -112,9 +112,9 @@ public function test_index_limits_per_page_to_maximum(): void $response = $this->getJson('/api/v1/logs?per_page=150'); $response->assertStatus(200); - + $pagination = $response->json('data.pagination'); - + // Should be limited to 100 as per controller logic $this->assertEquals(100, $pagination['per_page']); } @@ -126,9 +126,9 @@ public function test_index_uses_default_per_page_when_not_specified(): void $response = $this->getJson('/api/v1/logs'); $response->assertStatus(200); - + $pagination = $response->json('data.pagination'); - + // Should use default of 20 $this->assertEquals(20, $pagination['per_page']); } @@ -147,8 +147,8 @@ public function test_index_handles_empty_logs(): void 'total' => 0, 'current_page' => 1, 'last_page' => 1, - ] - ] + ], + ], ]); } @@ -159,7 +159,7 @@ public function test_index_pagination_works_correctly(): void // Test first page $response = $this->getJson('/api/v1/logs?per_page=10&page=1'); $response->assertStatus(200); - + $pagination = $response->json('data.pagination'); $this->assertEquals(1, $pagination['current_page']); $this->assertEquals(3, $pagination['last_page']); @@ -169,7 +169,7 @@ public function test_index_pagination_works_correctly(): void // Test second page $response = $this->getJson('/api/v1/logs?per_page=10&page=2'); $response->assertStatus(200); - + $pagination = $response->json('data.pagination'); $this->assertEquals(2, $pagination['current_page']); $this->assertEquals(11, $pagination['from']); @@ -186,15 +186,15 @@ public function test_index_with_multiple_log_levels(): void $response = $this->getJson('/api/v1/logs'); $response->assertStatus(200); - + $logs = $response->json('data.logs'); - + $this->assertCount(4, $logs); - + $levels = array_column($logs, 'level'); $this->assertContains('error', $levels); $this->assertContains('warning', $levels); $this->assertContains('info', $levels); $this->assertContains('debug', $levels); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php index 67f4a66..9ee425d 100644 --- a/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/OnboardingControllerTest.php @@ -19,7 +19,7 @@ class OnboardingControllerTest extends TestCase protected function setUp(): void { parent::setUp(); - + // Create a language for testing Language::factory()->create([ 'id' => 1, @@ -30,7 +30,7 @@ protected function setUp(): void ]); } - public function test_status_shows_needs_onboarding_when_no_components_exist() + public function test_status_shows_needs_onboarding_when_no_components_exist(): void { $response = $this->getJson('/api/v1/onboarding/status'); @@ -49,7 +49,7 @@ public function test_status_shows_needs_onboarding_when_no_components_exist() ]); } - public function test_status_shows_feed_step_when_platform_account_exists() + public function test_status_shows_feed_step_when_platform_account_exists(): void { PlatformAccount::factory()->create(['is_active' => true]); @@ -69,7 +69,7 @@ public function test_status_shows_feed_step_when_platform_account_exists() ]); } - public function test_status_shows_channel_step_when_platform_account_and_feed_exist() + public function test_status_shows_channel_step_when_platform_account_and_feed_exist(): void { $language = Language::first(); PlatformAccount::factory()->create(['is_active' => true]); @@ -91,7 +91,7 @@ public function test_status_shows_channel_step_when_platform_account_and_feed_ex ]); } - public function test_status_shows_route_step_when_platform_account_feed_and_channel_exist() + public function test_status_shows_route_step_when_platform_account_feed_and_channel_exist(): void { $language = Language::first(); PlatformAccount::factory()->create(['is_active' => true]); @@ -114,7 +114,7 @@ public function test_status_shows_route_step_when_platform_account_feed_and_chan ]); } - public function test_status_shows_no_onboarding_needed_when_all_components_exist() + public function test_status_shows_no_onboarding_needed_when_all_components_exist(): void { $language = Language::first(); PlatformAccount::factory()->create(['is_active' => true]); @@ -138,7 +138,7 @@ public function test_status_shows_no_onboarding_needed_when_all_components_exist ]); } - public function test_status_shows_no_onboarding_needed_when_skipped() + public function test_status_shows_no_onboarding_needed_when_skipped(): void { // No components exist but onboarding is skipped Setting::create([ @@ -163,7 +163,7 @@ public function test_status_shows_no_onboarding_needed_when_skipped() ]); } - public function test_options_returns_languages_and_platform_instances() + public function test_options_returns_languages_and_platform_instances(): void { PlatformInstance::factory()->create([ 'platform' => 'lemmy', @@ -179,34 +179,34 @@ public function test_options_returns_languages_and_platform_instances() 'success', 'data' => [ 'languages' => [ - '*' => ['id', 'short_code', 'name', 'native_name', 'is_active'] + '*' => ['id', 'short_code', 'name', 'native_name', 'is_active'], ], 'platform_instances' => [ - '*' => ['id', 'platform', 'url', 'name', 'description', 'is_active'] - ] - ] + '*' => ['id', 'platform', 'url', 'name', 'description', 'is_active'], + ], + ], ]); } - public function test_complete_onboarding_returns_success() + public function test_complete_onboarding_returns_success(): void { $response = $this->postJson('/api/v1/onboarding/complete'); $response->assertStatus(200) ->assertJson([ 'success' => true, - 'data' => ['completed' => true] + 'data' => ['completed' => true], ]); } - public function test_skip_onboarding_creates_setting() + public function test_skip_onboarding_creates_setting(): void { $response = $this->postJson('/api/v1/onboarding/skip'); $response->assertStatus(200) ->assertJson([ 'success' => true, - 'data' => ['skipped' => true] + 'data' => ['skipped' => true], ]); $this->assertDatabaseHas('settings', [ @@ -215,7 +215,7 @@ public function test_skip_onboarding_creates_setting() ]); } - public function test_skip_onboarding_updates_existing_setting() + public function test_skip_onboarding_updates_existing_setting(): void { // Create existing setting with false value Setting::create([ @@ -236,7 +236,7 @@ public function test_skip_onboarding_updates_existing_setting() $this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count()); } - public function test_reset_skip_removes_setting() + public function test_reset_skip_removes_setting(): void { // Create skipped setting Setting::create([ @@ -249,7 +249,7 @@ public function test_reset_skip_removes_setting() $response->assertStatus(200) ->assertJson([ 'success' => true, - 'data' => ['reset' => true] + 'data' => ['reset' => true], ]); $this->assertDatabaseMissing('settings', [ @@ -257,18 +257,18 @@ public function test_reset_skip_removes_setting() ]); } - public function test_reset_skip_works_when_no_setting_exists() + public function test_reset_skip_works_when_no_setting_exists(): void { $response = $this->postJson('/api/v1/onboarding/reset-skip'); $response->assertStatus(200) ->assertJson([ 'success' => true, - 'data' => ['reset' => true] + 'data' => ['reset' => true], ]); } - public function test_onboarding_flow_integration() + public function test_onboarding_flow_integration(): void { // 1. Initial status - needs onboarding $response = $this->getJson('/api/v1/onboarding/status'); @@ -290,4 +290,4 @@ public function test_onboarding_flow_integration() $response = $this->getJson('/api/v1/onboarding/status'); $response->assertJson(['data' => ['needs_onboarding' => true, 'onboarding_skipped' => false]]); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/PlatformAccountsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/PlatformAccountsControllerTest.php index 511f826..314ba69 100644 --- a/tests/Feature/Http/Controllers/Api/V1/PlatformAccountsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/PlatformAccountsControllerTest.php @@ -33,12 +33,12 @@ public function test_index_returns_successful_response(): void 'is_active', 'created_at', 'updated_at', - ] - ] + ], + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Platform accounts retrieved successfully.' + 'message' => 'Platform accounts retrieved successfully.', ]); } @@ -75,11 +75,11 @@ public function test_store_creates_platform_account_successfully(): void 'is_active', 'created_at', 'updated_at', - ] + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Platform account created successfully!' + 'message' => 'Platform account created successfully!', ]); $this->assertDatabaseHas('platform_accounts', [ @@ -115,7 +115,7 @@ public function test_show_returns_platform_account_successfully(): void 'is_active', 'created_at', 'updated_at', - ] + ], ]) ->assertJson([ 'success' => true, @@ -123,7 +123,7 @@ public function test_show_returns_platform_account_successfully(): void 'data' => [ 'id' => $account->id, 'username' => $account->username, - ] + ], ]); } @@ -134,7 +134,7 @@ public function test_update_modifies_platform_account_successfully(): void $updateData = [ 'instance_url' => 'https://updated.example.com', 'username' => 'updateduser', - 'settings' => ['updated' => 'value'] + 'settings' => ['updated' => 'value'], ]; $response = $this->putJson("/api/v1/platform-accounts/{$account->id}", $updateData); @@ -142,7 +142,7 @@ public function test_update_modifies_platform_account_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Platform account updated successfully!' + 'message' => 'Platform account updated successfully!', ]); $this->assertDatabaseHas('platform_accounts', [ @@ -161,11 +161,11 @@ public function test_destroy_deletes_platform_account_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Platform account deleted successfully!' + 'message' => 'Platform account deleted successfully!', ]); $this->assertDatabaseMissing('platform_accounts', [ - 'id' => $account->id + 'id' => $account->id, ]); } @@ -182,7 +182,7 @@ public function test_set_active_activates_platform_account(): void $this->assertDatabaseHas('platform_accounts', [ 'id' => $account->id, - 'is_active' => true + 'is_active' => true, ]); } @@ -190,12 +190,12 @@ public function test_set_active_deactivates_other_accounts_of_same_platform(): v { $activeAccount = PlatformAccount::factory()->create([ 'platform' => 'lemmy', - 'is_active' => true + 'is_active' => true, ]); - + $newAccount = PlatformAccount::factory()->create([ 'platform' => 'lemmy', - 'is_active' => false + 'is_active' => false, ]); $response = $this->postJson("/api/v1/platform-accounts/{$newAccount->id}/set-active"); @@ -204,12 +204,12 @@ public function test_set_active_deactivates_other_accounts_of_same_platform(): v $this->assertDatabaseHas('platform_accounts', [ 'id' => $activeAccount->id, - 'is_active' => false + 'is_active' => false, ]); $this->assertDatabaseHas('platform_accounts', [ 'id' => $newAccount->id, - 'is_active' => true + 'is_active' => true, ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/PlatformChannelsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/PlatformChannelsControllerTest.php index b6e883e..84f06cb 100644 --- a/tests/Feature/Http/Controllers/Api/V1/PlatformChannelsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/PlatformChannelsControllerTest.php @@ -34,24 +34,24 @@ public function test_index_returns_successful_response(): void 'is_active', 'created_at', 'updated_at', - 'platform_instance' - ] - ] + 'platform_instance', + ], + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Platform channels retrieved successfully.' + 'message' => 'Platform channels retrieved successfully.', ]); } public function test_store_creates_platform_channel_successfully(): void { $instance = PlatformInstance::factory()->create(); - + // Create a platform account for this instance first PlatformAccount::factory()->create([ 'instance_url' => $instance->url, - 'is_active' => true + 'is_active' => true, ]); $data = [ @@ -76,11 +76,11 @@ public function test_store_creates_platform_channel_successfully(): void 'is_active', 'created_at', 'updated_at', - ] + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Platform channel created successfully and linked to platform account!' + 'message' => 'Platform channel created successfully and linked to platform account!', ]); $this->assertDatabaseHas('platform_channels', [ @@ -102,7 +102,7 @@ public function test_store_validates_platform_instance_exists(): void { $data = [ 'platform_instance_id' => 999, - 'name' => 'Test Channel' + 'name' => 'Test Channel', ]; $response = $this->postJson('/api/v1/platform-channels', $data); @@ -132,8 +132,8 @@ public function test_show_returns_platform_channel_successfully(): void 'is_active', 'created_at', 'updated_at', - 'platform_instance' - ] + 'platform_instance', + ], ]) ->assertJson([ 'success' => true, @@ -141,7 +141,7 @@ public function test_show_returns_platform_channel_successfully(): void 'data' => [ 'id' => $channel->id, 'name' => $channel->name, - ] + ], ]); } @@ -154,7 +154,7 @@ public function test_update_modifies_platform_channel_successfully(): void 'name' => 'Updated Channel', 'display_name' => 'Updated Display Name', 'description' => 'Updated description', - 'is_active' => false + 'is_active' => false, ]; $response = $this->putJson("/api/v1/platform-channels/{$channel->id}", $updateData); @@ -162,7 +162,7 @@ public function test_update_modifies_platform_channel_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Platform channel updated successfully!' + 'message' => 'Platform channel updated successfully!', ]); $this->assertDatabaseHas('platform_channels', [ @@ -183,11 +183,11 @@ public function test_destroy_deletes_platform_channel_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Platform channel deleted successfully!' + 'message' => 'Platform channel deleted successfully!', ]); $this->assertDatabaseMissing('platform_channels', [ - 'id' => $channel->id + 'id' => $channel->id, ]); } @@ -196,7 +196,7 @@ public function test_toggle_activates_inactive_channel(): void $instance = PlatformInstance::factory()->create(); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'is_active' => false + 'is_active' => false, ]); $response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle"); @@ -204,12 +204,12 @@ public function test_toggle_activates_inactive_channel(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Platform channel activated successfully!' + 'message' => 'Platform channel activated successfully!', ]); $this->assertDatabaseHas('platform_channels', [ 'id' => $channel->id, - 'is_active' => true + 'is_active' => true, ]); } @@ -218,7 +218,7 @@ public function test_toggle_deactivates_active_channel(): void $instance = PlatformInstance::factory()->create(); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'is_active' => true + 'is_active' => true, ]); $response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle"); @@ -226,12 +226,12 @@ public function test_toggle_deactivates_active_channel(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Platform channel deactivated successfully!' + 'message' => 'Platform channel deactivated successfully!', ]); $this->assertDatabaseHas('platform_channels', [ 'id' => $channel->id, - 'is_active' => false + 'is_active' => false, ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/RoutingControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/RoutingControllerTest.php index c8356ea..750d1b8 100644 --- a/tests/Feature/Http/Controllers/Api/V1/RoutingControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/RoutingControllerTest.php @@ -18,18 +18,18 @@ public function test_index_returns_successful_response(): void { $language = Language::factory()->create(); $instance = PlatformInstance::factory()->create(); - + // Create unique feeds and channels for this test $feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]); $channels = PlatformChannel::factory()->count(3)->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); - + foreach ($feeds as $index => $feed) { Route::factory()->create([ 'feed_id' => $feed->id, - 'platform_channel_id' => $channels[$index]->id + 'platform_channel_id' => $channels[$index]->id, ]); } @@ -47,12 +47,12 @@ public function test_index_returns_successful_response(): void 'priority', 'created_at', 'updated_at', - ] - ] + ], + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Routing configurations retrieved successfully.' + 'message' => 'Routing configurations retrieved successfully.', ]); } @@ -63,14 +63,14 @@ public function test_store_creates_routing_configuration_successfully(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $data = [ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 5 + 'priority' => 5, ]; $response = $this->postJson('/api/v1/routing', $data); @@ -86,11 +86,11 @@ public function test_store_creates_routing_configuration_successfully(): void 'priority', 'created_at', 'updated_at', - ] + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Routing configuration created successfully!' + 'message' => 'Routing configuration created successfully!', ]); $this->assertDatabaseHas('routes', [ @@ -115,12 +115,12 @@ public function test_store_validates_feed_exists(): void $instance = PlatformInstance::factory()->create(); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $data = [ 'feed_id' => 999, - 'platform_channel_id' => $channel->id + 'platform_channel_id' => $channel->id, ]; $response = $this->postJson('/api/v1/routing', $data); @@ -136,7 +136,7 @@ public function test_store_validates_platform_channel_exists(): void $data = [ 'feed_id' => $feed->id, - 'platform_channel_id' => 999 + 'platform_channel_id' => 999, ]; $response = $this->postJson('/api/v1/routing', $data); @@ -152,12 +152,12 @@ public function test_show_returns_routing_configuration_successfully(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $route = Route::factory()->create([ 'feed_id' => $feed->id, - 'platform_channel_id' => $channel->id + 'platform_channel_id' => $channel->id, ]); $response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}"); @@ -173,11 +173,11 @@ public function test_show_returns_routing_configuration_successfully(): void 'priority', 'created_at', 'updated_at', - ] + ], ]) ->assertJson([ 'success' => true, - 'message' => 'Routing configuration retrieved successfully.' + 'message' => 'Routing configuration retrieved successfully.', ]); } @@ -188,7 +188,7 @@ public function test_show_returns_404_for_nonexistent_routing(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}"); @@ -196,7 +196,7 @@ public function test_show_returns_404_for_nonexistent_routing(): void $response->assertStatus(404) ->assertJson([ 'success' => false, - 'message' => 'Routing configuration not found.' + 'message' => 'Routing configuration not found.', ]); } @@ -207,19 +207,19 @@ public function test_update_modifies_routing_configuration_successfully(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $route = Route::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 1 + 'priority' => 1, ]); $updateData = [ 'is_active' => false, - 'priority' => 10 + 'priority' => 10, ]; $response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", $updateData); @@ -227,7 +227,7 @@ public function test_update_modifies_routing_configuration_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Routing configuration updated successfully!' + 'message' => 'Routing configuration updated successfully!', ]); $this->assertDatabaseHas('routes', [ @@ -245,17 +245,17 @@ public function test_update_returns_404_for_nonexistent_routing(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [ - 'is_active' => false + 'is_active' => false, ]); $response->assertStatus(404) ->assertJson([ 'success' => false, - 'message' => 'Routing configuration not found.' + 'message' => 'Routing configuration not found.', ]); } @@ -266,12 +266,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $route = Route::factory()->create([ 'feed_id' => $feed->id, - 'platform_channel_id' => $channel->id + 'platform_channel_id' => $channel->id, ]); $response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}"); @@ -279,12 +279,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Routing configuration deleted successfully!' + 'message' => 'Routing configuration deleted successfully!', ]); $this->assertDatabaseMissing('routes', [ 'feed_id' => $feed->id, - 'platform_channel_id' => $channel->id + 'platform_channel_id' => $channel->id, ]); } @@ -295,7 +295,7 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}"); @@ -303,7 +303,7 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void $response->assertStatus(404) ->assertJson([ 'success' => false, - 'message' => 'Routing configuration not found.' + 'message' => 'Routing configuration not found.', ]); } @@ -314,13 +314,13 @@ public function test_toggle_activates_inactive_routing(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $route = Route::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'is_active' => false + 'is_active' => false, ]); $response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle"); @@ -328,13 +328,13 @@ public function test_toggle_activates_inactive_routing(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Routing configuration activated successfully!' + 'message' => 'Routing configuration activated successfully!', ]); $this->assertDatabaseHas('routes', [ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'is_active' => true + 'is_active' => true, ]); } @@ -345,13 +345,13 @@ public function test_toggle_deactivates_active_routing(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $route = Route::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'is_active' => true + 'is_active' => true, ]); $response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle"); @@ -359,13 +359,13 @@ public function test_toggle_deactivates_active_routing(): void $response->assertStatus(200) ->assertJson([ 'success' => true, - 'message' => 'Routing configuration deactivated successfully!' + 'message' => 'Routing configuration deactivated successfully!', ]); $this->assertDatabaseHas('routes', [ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'is_active' => false + 'is_active' => false, ]); } @@ -376,7 +376,7 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void $feed = Feed::factory()->create(['language_id' => $language->id]); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'language_id' => $language->id + 'language_id' => $language->id, ]); $response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle"); @@ -384,7 +384,7 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void $response->assertStatus(404) ->assertJson([ 'success' => false, - 'message' => 'Routing configuration not found.' + 'message' => 'Routing configuration not found.', ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/Http/Controllers/Api/V1/SettingsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/SettingsControllerTest.php index e41b8c3..d2a6582 100644 --- a/tests/Feature/Http/Controllers/Api/V1/SettingsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/SettingsControllerTest.php @@ -22,11 +22,11 @@ public function test_index_returns_current_settings(): void 'publishing_approvals_enabled', 'article_publishing_interval', ], - 'message' + 'message', ]) ->assertJson([ 'success' => true, - 'message' => 'Settings retrieved successfully.' + 'message' => 'Settings retrieved successfully.', ]); } @@ -42,7 +42,7 @@ public function test_update_modifies_article_processing_setting(): void 'message' => 'Settings updated successfully.', 'data' => [ 'article_processing_enabled' => false, - ] + ], ]); } @@ -58,7 +58,7 @@ public function test_update_modifies_publishing_approvals_setting(): void 'message' => 'Settings updated successfully.', 'data' => [ 'publishing_approvals_enabled' => true, - ] + ], ]); } @@ -72,7 +72,7 @@ public function test_update_validates_boolean_values(): void $response->assertStatus(422) ->assertJsonValidationErrors([ 'article_processing_enabled', - 'publishing_approvals_enabled' + 'publishing_approvals_enabled', ]); } @@ -88,7 +88,7 @@ public function test_update_accepts_partial_updates(): void 'success' => true, 'data' => [ 'article_processing_enabled' => true, - ] + ], ]); // Should still have structure for all settings @@ -97,7 +97,7 @@ public function test_update_accepts_partial_updates(): void 'article_processing_enabled', 'publishing_approvals_enabled', 'article_publishing_interval', - ] + ], ]); } diff --git a/tests/Feature/JobsAndEventsTest.php b/tests/Feature/JobsAndEventsTest.php index c73ce60..ed85e4e 100644 --- a/tests/Feature/JobsAndEventsTest.php +++ b/tests/Feature/JobsAndEventsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Events\ActionPerformed; use App\Events\ArticleApproved; // use App\Events\ArticleReadyToPublish; // Class no longer exists use App\Events\ExceptionLogged; @@ -18,11 +19,10 @@ use App\Models\Article; use App\Models\Feed; use App\Models\Log; -use App\Models\Setting; use App\Models\PlatformChannel; -use App\Services\Log\LogSaver; +use App\Models\Setting; use App\Services\Article\ArticleFetcher; -use App\Services\Article\ValidationService; +use App\Services\Log\LogSaver; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; @@ -44,7 +44,7 @@ public function test_article_discovery_job_processes_successfully(): void $feed = Feed::factory()->create(['is_active' => true]); $logSaver = app(LogSaver::class); - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; $job->handle($logSaver); // Should dispatch individual feed jobs @@ -57,7 +57,7 @@ public function test_article_discovery_for_feed_job_processes_feed(): void $feed = Feed::factory()->create([ 'url' => 'https://example.com/feed', - 'is_active' => true + 'is_active' => true, ]); // Mock the ArticleFetcher service in the container @@ -94,10 +94,9 @@ public function test_sync_channel_posts_job_processes_successfully(): void $this->assertTrue(true); } - public function test_publish_next_article_job_has_correct_configuration(): void { - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; $this->assertEquals('publishing', $job->queue); $this->assertInstanceOf(PublishNextArticleJob::class, $job); @@ -164,12 +163,12 @@ public function test_exception_logged_event_is_dispatched(): void $log = Log::factory()->create([ 'level' => 'error', 'message' => 'Test error', - 'context' => json_encode(['key' => 'value']) + 'context' => json_encode(['key' => 'value']), ]); event(new ExceptionLogged($log)); - Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) use ($log) { + Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) { return $event->log->message === 'Test error'; }); } @@ -185,7 +184,7 @@ public function test_validate_article_listener_processes_new_article(): void $article = Article::factory()->create([ 'feed_id' => $feed->id, 'approval_status' => 'pending', - ]); + ]); // Mock ArticleFetcher to return valid article data $mockFetcher = \Mockery::mock(\App\Services\Article\ArticleFetcher::class); @@ -195,7 +194,7 @@ public function test_validate_article_listener_processes_new_article(): void ->andReturn([ 'title' => 'Belgian News', 'description' => 'News from Belgium', - 'full_article' => 'This is a test article about Belgium and Belgian politics.' + 'full_article' => 'This is a test article about Belgium and Belgian politics.', ]); $listener = app(ValidateArticleListener::class); @@ -248,10 +247,10 @@ public function test_log_exception_to_database_listener_creates_log(): void $log = Log::factory()->create([ 'level' => 'error', 'message' => 'Test exception message', - 'context' => json_encode(['error' => 'details']) + 'context' => json_encode(['error' => 'details']), ]); - $listener = new LogExceptionToDatabase(); + $listener = new LogExceptionToDatabase; $exception = new \Exception('Test exception message'); $event = new ExceptionOccurred($exception, \App\Enums\LogLevelEnum::ERROR, 'Test exception message'); @@ -259,7 +258,7 @@ public function test_log_exception_to_database_listener_creates_log(): void $this->assertDatabaseHas('logs', [ 'level' => 'error', - 'message' => 'Test exception message' + 'message' => 'Test exception message', ]); $savedLog = Log::where('message', 'Test exception message')->first(); @@ -270,6 +269,9 @@ public function test_log_exception_to_database_listener_creates_log(): void public function test_event_listener_registration_works(): void { // Test that events are properly bound to listeners + $listeners = Event::getListeners(ActionPerformed::class); + $this->assertNotEmpty($listeners); + $listeners = Event::getListeners(NewArticleFetched::class); $this->assertNotEmpty($listeners); @@ -287,7 +289,7 @@ public function test_event_listener_registration_works(): void public function test_job_retry_configuration(): void { - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; // Test that job has unique configuration $this->assertObjectHasProperty('uniqueFor', $job); @@ -300,9 +302,9 @@ public function test_job_queue_configuration(): void $channel = PlatformChannel::factory()->create(); $article = Article::factory()->create(['feed_id' => $feed->id]); - $discoveryJob = new ArticleDiscoveryJob(); + $discoveryJob = new ArticleDiscoveryJob; $feedJob = new ArticleDiscoveryForFeedJob($feed); - $publishJob = new PublishNextArticleJob(); + $publishJob = new PublishNextArticleJob; $syncJob = new SyncChannelPostsJob($channel); // Test queue assignments diff --git a/tests/Feature/ValidateArticleListenerTest.php b/tests/Feature/ValidateArticleListenerTest.php index 5eccff9..cf7366f 100644 --- a/tests/Feature/ValidateArticleListenerTest.php +++ b/tests/Feature/ValidateArticleListenerTest.php @@ -2,7 +2,7 @@ namespace Tests\Feature; -use App\Events\ArticleReadyToPublish; +use App\Events\ArticleApproved; use App\Events\NewArticleFetched; use App\Listeners\ValidateArticleListener; use App\Models\Article; @@ -20,11 +20,11 @@ class ValidateArticleListenerTest extends TestCase public function test_listener_validates_article_and_dispatches_ready_to_publish_event(): void { - Event::fake([ArticleReadyToPublish::class]); + Event::fake([ArticleApproved::class]); // Mock HTTP requests Http::fake([ - 'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200) + 'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200), ]); $feed = Feed::factory()->create(); @@ -42,17 +42,17 @@ public function test_listener_validates_article_and_dispatches_ready_to_publish_ $article->refresh(); if ($article->isValid()) { - Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) { + Event::assertDispatched(ArticleApproved::class, function (ArticleApproved $event) use ($article) { return $event->article->id === $article->id; }); } else { - Event::assertNotDispatched(ArticleReadyToPublish::class); + Event::assertNotDispatched(ArticleApproved::class); } } public function test_listener_skips_already_validated_articles(): void { - Event::fake([ArticleReadyToPublish::class]); + Event::fake([ArticleApproved::class]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ @@ -66,12 +66,12 @@ public function test_listener_skips_already_validated_articles(): void $listener->handle($event); - Event::assertNotDispatched(ArticleReadyToPublish::class); + Event::assertNotDispatched(ArticleApproved::class); } public function test_listener_skips_articles_with_existing_publication(): void { - Event::fake([ArticleReadyToPublish::class]); + Event::fake([ArticleApproved::class]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ @@ -93,16 +93,16 @@ public function test_listener_skips_articles_with_existing_publication(): void $listener->handle($event); - Event::assertNotDispatched(ArticleReadyToPublish::class); + Event::assertNotDispatched(ArticleApproved::class); } public function test_listener_calls_validation_service(): void { - Event::fake([ArticleReadyToPublish::class]); + Event::fake([ArticleApproved::class]); // Mock HTTP requests Http::fake([ - 'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200) + 'https://example.com/article' => Http::response('<html><body>Article content</body></html>', 200), ]); $feed = Feed::factory()->create(); diff --git a/tests/TestCase.php b/tests/TestCase.php index da6d365..b22dca3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,8 +3,8 @@ namespace Tests; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Facade; +use Illuminate\Support\Facades\Http; use Mockery; abstract class TestCase extends BaseTestCase @@ -14,13 +14,16 @@ abstract class TestCase extends BaseTestCase protected function setUp(): void { parent::setUp(); - + + // Prevent Vite manifest errors in tests (no npm build in CI) + $this->withoutVite(); + // Clean up any existing Mockery instances before each test if (class_exists(Mockery::class)) { Mockery::close(); Mockery::globalHelpers(); } - + // Prevent any external HTTP requests during tests unless explicitly faked in a test Http::preventStrayRequests(); } @@ -29,15 +32,15 @@ protected function tearDown(): void { // Clear HTTP fakes between tests to prevent interference Http::clearResolvedInstances(); - + // Clear all facade instances to prevent interference Facade::clearResolvedInstances(); - + // Ensure Mockery is properly closed to prevent facade interference if (class_exists(Mockery::class)) { Mockery::close(); } - + parent::tearDown(); } } diff --git a/tests/Traits/CreatesArticleFetcher.php b/tests/Traits/CreatesArticleFetcher.php index dcd38eb..ad2f985 100644 --- a/tests/Traits/CreatesArticleFetcher.php +++ b/tests/Traits/CreatesArticleFetcher.php @@ -10,7 +10,7 @@ trait CreatesArticleFetcher { protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetcher { - if (!$logSaver) { + if (! $logSaver) { $logSaver = Mockery::mock(LogSaver::class); $logSaver->shouldReceive('info')->zeroOrMoreTimes(); $logSaver->shouldReceive('warning')->zeroOrMoreTimes(); @@ -21,6 +21,7 @@ protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetc return new ArticleFetcher($logSaver); } + /** @return array{ArticleFetcher, \Mockery\MockInterface} */ protected function createArticleFetcherWithMockedLogSaver(): array { $logSaver = Mockery::mock(LogSaver::class); @@ -28,9 +29,9 @@ protected function createArticleFetcherWithMockedLogSaver(): array $logSaver->shouldReceive('warning')->zeroOrMoreTimes(); $logSaver->shouldReceive('error')->zeroOrMoreTimes(); $logSaver->shouldReceive('debug')->zeroOrMoreTimes(); - + $articleFetcher = new ArticleFetcher($logSaver); - + return [$articleFetcher, $logSaver]; } -} \ No newline at end of file +} diff --git a/tests/Unit/Actions/CreateChannelActionTest.php b/tests/Unit/Actions/CreateChannelActionTest.php index f100a95..06160f7 100644 --- a/tests/Unit/Actions/CreateChannelActionTest.php +++ b/tests/Unit/Actions/CreateChannelActionTest.php @@ -19,7 +19,7 @@ class CreateChannelActionTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->action = new CreateChannelAction(); + $this->action = new CreateChannelAction; } public function test_creates_channel_and_attaches_account(): void diff --git a/tests/Unit/Actions/CreateFeedActionTest.php b/tests/Unit/Actions/CreateFeedActionTest.php index 5c4eec6..accb3f5 100644 --- a/tests/Unit/Actions/CreateFeedActionTest.php +++ b/tests/Unit/Actions/CreateFeedActionTest.php @@ -17,7 +17,7 @@ class CreateFeedActionTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->action = new CreateFeedAction(); + $this->action = new CreateFeedAction; } public function test_creates_vrt_feed_with_correct_url(): void @@ -42,8 +42,8 @@ public function test_creates_belga_feed_with_correct_url(): void $feed = $this->action->execute('Belga News', 'belga', $language->id); - $this->assertEquals('https://www.belganewsagency.eu/', $feed->url); - $this->assertEquals('website', $feed->type); + $this->assertEquals('https://www.belganewsagency.eu/feed', $feed->url); + $this->assertEquals('rss', $feed->type); $this->assertEquals('belga', $feed->provider); $this->assertNull($feed->description); } diff --git a/tests/Unit/Actions/CreatePlatformAccountActionTest.php b/tests/Unit/Actions/CreatePlatformAccountActionTest.php index 5d01af3..051c522 100644 --- a/tests/Unit/Actions/CreatePlatformAccountActionTest.php +++ b/tests/Unit/Actions/CreatePlatformAccountActionTest.php @@ -17,6 +17,8 @@ class CreatePlatformAccountActionTest extends TestCase use RefreshDatabase; private CreatePlatformAccountAction $action; + + /** @var LemmyAuthService&\Mockery\MockInterface */ private LemmyAuthService $lemmyAuthService; protected function setUp(): void @@ -56,7 +58,6 @@ public function test_creates_platform_account_with_new_instance(): void $this->assertEquals('Test User', $account->settings['display_name']); $this->assertEquals('A test bio', $account->settings['description']); $this->assertEquals('test-jwt-token', $account->settings['api_token']); - $this->assertDatabaseHas('platform_instances', [ 'url' => 'https://lemmy.world', 'platform' => 'lemmy', diff --git a/tests/Unit/Actions/CreateRouteActionTest.php b/tests/Unit/Actions/CreateRouteActionTest.php index 0012237..2bfa248 100644 --- a/tests/Unit/Actions/CreateRouteActionTest.php +++ b/tests/Unit/Actions/CreateRouteActionTest.php @@ -19,7 +19,7 @@ class CreateRouteActionTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->action = new CreateRouteAction(); + $this->action = new CreateRouteAction; } public function test_creates_route_with_defaults(): void diff --git a/tests/Unit/Enums/LogLevelEnumTest.php b/tests/Unit/Enums/LogLevelEnumTest.php index 88084f3..bb127a1 100644 --- a/tests/Unit/Enums/LogLevelEnumTest.php +++ b/tests/Unit/Enums/LogLevelEnumTest.php @@ -28,7 +28,7 @@ public function test_to_array_returns_all_enum_values(): void public function test_enum_cases_exist(): void { $cases = LogLevelEnum::cases(); - + $this->assertCount(5, $cases); $this->assertContains(LogLevelEnum::DEBUG, $cases); $this->assertContains(LogLevelEnum::INFO, $cases); @@ -105,4 +105,4 @@ public function test_enum_can_be_used_in_match_expression(): void $this->assertEquals('Error message', $getMessage(LogLevelEnum::ERROR)); $this->assertEquals('Critical message', $getMessage(LogLevelEnum::CRITICAL)); } -} \ No newline at end of file +} diff --git a/tests/Unit/Enums/PlatformEnumTest.php b/tests/Unit/Enums/PlatformEnumTest.php index ebd9348..9999df2 100644 --- a/tests/Unit/Enums/PlatformEnumTest.php +++ b/tests/Unit/Enums/PlatformEnumTest.php @@ -15,7 +15,7 @@ public function test_enum_cases_have_correct_values(): void public function test_enum_cases_exist(): void { $cases = PlatformEnum::cases(); - + $this->assertCount(1, $cases); $this->assertContains(PlatformEnum::LEMMY, $cases); } @@ -86,4 +86,14 @@ public function test_enum_value_is_string_backed(): void { $this->assertIsString(PlatformEnum::LEMMY->value); } -} \ No newline at end of file + + public function test_channel_label_returns_community_for_lemmy(): void + { + $this->assertEquals('Community', PlatformEnum::LEMMY->channelLabel()); + } + + public function test_channel_label_plural_returns_communities_for_lemmy(): void + { + $this->assertEquals('Communities', PlatformEnum::LEMMY->channelLabelPlural()); + } +} diff --git a/tests/Unit/Events/ActionPerformedTest.php b/tests/Unit/Events/ActionPerformedTest.php new file mode 100644 index 0000000..5091bbf --- /dev/null +++ b/tests/Unit/Events/ActionPerformedTest.php @@ -0,0 +1,48 @@ +<?php + +namespace Tests\Unit\Events; + +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; +use Illuminate\Foundation\Events\Dispatchable; +use Tests\TestCase; + +class ActionPerformedTest extends TestCase +{ + public function test_event_can_be_constructed_with_defaults(): void + { + $event = new ActionPerformed('Test message'); + + $this->assertEquals('Test message', $event->message); + $this->assertEquals(LogLevelEnum::INFO, $event->level); + $this->assertEquals([], $event->context); + } + + public function test_event_can_be_constructed_with_custom_level_and_context(): void + { + $context = ['article_id' => 1, 'error' => 'Something failed']; + + $event = new ActionPerformed( + 'Article validation failed', + LogLevelEnum::ERROR, + $context, + ); + + $this->assertEquals('Article validation failed', $event->message); + $this->assertEquals(LogLevelEnum::ERROR, $event->level); + $this->assertEquals($context, $event->context); + } + + public function test_event_uses_dispatchable_trait(): void + { + $this->assertContains(Dispatchable::class, class_uses(ActionPerformed::class)); + } + + public function test_event_does_not_use_serializes_models_trait(): void + { + $this->assertNotContains( + \Illuminate\Queue\SerializesModels::class, + class_uses(ActionPerformed::class), + ); + } +} diff --git a/tests/Unit/Exceptions/RoutingMismatchExceptionTest.php b/tests/Unit/Exceptions/RoutingMismatchExceptionTest.php index 2b6dd9e..b88968d 100644 --- a/tests/Unit/Exceptions/RoutingMismatchExceptionTest.php +++ b/tests/Unit/Exceptions/RoutingMismatchExceptionTest.php @@ -18,10 +18,10 @@ public function test_exception_constructs_with_correct_message(): void // Arrange $englishLang = Language::factory()->create(['short_code' => 'en', 'name' => 'English']); $frenchLang = Language::factory()->create(['short_code' => 'fr', 'name' => 'French']); - + $feed = new Feed(['name' => 'Test Feed']); $feed->setRelation('language', $englishLang); - + $channel = new PlatformChannel(['name' => 'Test Channel']); $channel->setRelation('language', $frenchLang); @@ -41,10 +41,10 @@ public function test_exception_extends_routing_exception(): void // Arrange $englishLang = Language::factory()->create(['short_code' => 'en']); $frenchLang = Language::factory()->create(['short_code' => 'fr']); - + $feed = new Feed(['name' => 'Test Feed']); $feed->setRelation('language', $englishLang); - + $channel = new PlatformChannel(['name' => 'Test Channel']); $channel->setRelation('language', $frenchLang); @@ -60,10 +60,10 @@ public function test_exception_with_different_languages(): void // Arrange $dutchLang = Language::factory()->create(['short_code' => 'nl', 'name' => 'Dutch']); $germanLang = Language::factory()->create(['short_code' => 'de', 'name' => 'German']); - + $feed = new Feed(['name' => 'Dutch News']); $feed->setRelation('language', $dutchLang); - + $channel = new PlatformChannel(['name' => 'German Channel']); $channel->setRelation('language', $germanLang); @@ -82,10 +82,10 @@ public function test_exception_message_contains_all_required_elements(): void // Arrange $frenchLang = Language::factory()->create(['short_code' => 'fr', 'name' => 'French']); $spanishLang = Language::factory()->create(['short_code' => 'es', 'name' => 'Spanish']); - + $feed = new Feed(['name' => 'French Feed']); $feed->setRelation('language', $frenchLang); - + $channel = new PlatformChannel(['name' => 'Spanish Channel']); $channel->setRelation('language', $spanishLang); @@ -105,7 +105,7 @@ public function test_exception_with_null_languages(): void // Arrange $feed = new Feed(['name' => 'No Lang Feed']); $feed->setRelation('language', null); - + $channel = new PlatformChannel(['name' => 'No Lang Channel']); $channel->setRelation('language', null); @@ -124,10 +124,10 @@ public function test_exception_with_special_characters_in_names(): void // Arrange $englishLang = Language::factory()->create(['short_code' => 'en']); $frenchLang = Language::factory()->create(['short_code' => 'fr']); - + $feed = new Feed(['name' => 'Feed with "quotes" & symbols']); $feed->setRelation('language', $englishLang); - + $channel = new PlatformChannel(['name' => 'Channel with <tags>']); $channel->setRelation('language', $frenchLang); @@ -146,17 +146,17 @@ public function test_exception_is_throwable(): void // Arrange $englishLang = Language::factory()->create(['short_code' => 'en']); $frenchLang = Language::factory()->create(['short_code' => 'fr']); - + $feed = new Feed(['name' => 'Test Feed']); $feed->setRelation('language', $englishLang); - + $channel = new PlatformChannel(['name' => 'Test Channel']); $channel->setRelation('language', $frenchLang); // Act & Assert $this->expectException(RoutingMismatchException::class); $this->expectExceptionMessage('Language mismatch'); - + throw new RoutingMismatchException($feed, $channel); } -} \ No newline at end of file +} diff --git a/tests/Unit/Facades/LogSaverTest.php b/tests/Unit/Facades/LogSaverTest.php index db240c2..0649491 100644 --- a/tests/Unit/Facades/LogSaverTest.php +++ b/tests/Unit/Facades/LogSaverTest.php @@ -3,11 +3,11 @@ namespace Tests\Unit\Facades; use App\Enums\LogLevelEnum; +use App\Enums\PlatformEnum; use App\Facades\LogSaver; use App\Models\Log; use App\Models\PlatformChannel; use App\Models\PlatformInstance; -use App\Enums\PlatformEnum; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -20,7 +20,7 @@ public function test_facade_accessor_returns_correct_service(): void $reflection = new \ReflectionClass(LogSaver::class); $method = $reflection->getMethod('getFacadeAccessor'); $method->setAccessible(true); - + $this->assertEquals(\App\Services\Log\LogSaver::class, $method->invoke(null)); } @@ -92,12 +92,12 @@ public function test_facade_works_with_channel(): void { $platformInstance = PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'url' => 'https://facade.test.com' + 'url' => 'https://facade.test.com', ]); $channel = PlatformChannel::factory()->create([ 'name' => 'Facade Test Channel', - 'platform_instance_id' => $platformInstance->id + 'platform_instance_id' => $platformInstance->id, ]); $message = 'Facade channel test'; @@ -125,11 +125,11 @@ public function test_facade_static_calls_resolve_to_service_instance(): void LogSaver::error('Test message 2'); $this->assertDatabaseCount('logs', 2); - + $logs = Log::orderBy('id')->get(); $this->assertEquals('Test message 1', $logs[0]->message); $this->assertEquals('Test message 2', $logs[1]->message); $this->assertEquals(LogLevelEnum::INFO, $logs[0]->level); $this->assertEquals(LogLevelEnum::ERROR, $logs[1]->level); } -} \ No newline at end of file +} diff --git a/tests/Unit/Jobs/ArticleDiscoveryForFeedJobTest.php b/tests/Unit/Jobs/ArticleDiscoveryForFeedJobTest.php index e33e3a6..c1c5579 100644 --- a/tests/Unit/Jobs/ArticleDiscoveryForFeedJobTest.php +++ b/tests/Unit/Jobs/ArticleDiscoveryForFeedJobTest.php @@ -25,7 +25,7 @@ public function test_constructor_sets_correct_queue(): void { $feed = Feed::factory()->make(); $job = new ArticleDiscoveryForFeedJob($feed); - + $this->assertEquals('feed-discovery', $job->queue); } @@ -33,7 +33,7 @@ public function test_job_implements_should_queue(): void { $feed = Feed::factory()->make(); $job = new ArticleDiscoveryForFeedJob($feed); - + $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); } @@ -41,7 +41,7 @@ public function test_job_uses_queueable_trait(): void { $feed = Feed::factory()->make(); $job = new ArticleDiscoveryForFeedJob($feed); - + $this->assertContains( \Illuminate\Foundation\Queue\Queueable::class, class_uses($job) @@ -54,7 +54,7 @@ public function test_handle_fetches_articles_and_updates_feed(): void $feed = Feed::factory()->create([ 'name' => 'Test Feed', 'url' => 'https://example.com/feed', - 'last_fetched_at' => null + 'last_fetched_at' => null, ]); $mockArticles = collect(['article1', 'article2']); @@ -72,15 +72,15 @@ public function test_handle_fetches_articles_and_updates_feed(): void ->with('Starting feed article fetch', null, [ 'feed_id' => $feed->id, 'feed_name' => $feed->name, - 'feed_url' => $feed->url + 'feed_url' => $feed->url, ]) ->once(); - + $logSaverMock->shouldReceive('info') ->with('Feed article fetch completed', null, [ 'feed_id' => $feed->id, 'feed_name' => $feed->name, - 'articles_count' => 2 + 'articles_count' => 2, ]) ->once(); @@ -106,7 +106,7 @@ public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay(): $logSaverMock->shouldReceive('info') ->times(3) // Once for each active feed ->with('Dispatched feed discovery job', null, Mockery::type('array')); - + $this->app->instance(LogSaver::class, $logSaverMock); // Act @@ -114,7 +114,7 @@ public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay(): // Assert Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 3); - + // Verify jobs were dispatched (cannot access private $feed property in test) } @@ -126,7 +126,7 @@ public function test_dispatch_for_all_active_feeds_applies_correct_delays(): voi // Mock LogSaver $logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock->shouldReceive('info')->times(2); - + $this->app->instance(LogSaver::class, $logSaverMock); // Act @@ -134,7 +134,7 @@ public function test_dispatch_for_all_active_feeds_applies_correct_delays(): voi // Assert Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 2); - + // Verify jobs are pushed with delays Queue::assertPushed(ArticleDiscoveryForFeedJob::class, function ($job) { return $job->delay !== null; @@ -157,7 +157,7 @@ public function test_feed_discovery_delay_constant_exists(): void { $reflection = new \ReflectionClass(ArticleDiscoveryForFeedJob::class); $constant = $reflection->getConstant('FEED_DISCOVERY_DELAY_MINUTES'); - + $this->assertEquals(5, $constant); } @@ -165,10 +165,10 @@ public function test_job_can_be_serialized(): void { $feed = Feed::factory()->create(['name' => 'Test Feed']); $job = new ArticleDiscoveryForFeedJob($feed); - + $serialized = serialize($job); $unserialized = unserialize($serialized); - + $this->assertInstanceOf(ArticleDiscoveryForFeedJob::class, $unserialized); $this->assertEquals($job->queue, $unserialized->queue); // Note: Cannot test feed property directly as it's private @@ -180,7 +180,7 @@ public function test_handle_logs_start_message_with_correct_context(): void // Arrange $feed = Feed::factory()->create([ 'name' => 'Test Feed', - 'url' => 'https://example.com/feed' + 'url' => 'https://example.com/feed', ]); $mockArticles = collect([]); @@ -197,10 +197,10 @@ public function test_handle_logs_start_message_with_correct_context(): void ->with('Starting feed article fetch', null, [ 'feed_id' => $feed->id, 'feed_name' => 'Test Feed', - 'feed_url' => 'https://example.com/feed' + 'feed_url' => 'https://example.com/feed', ]) ->once(); - + $logSaverMock->shouldReceive('info') ->with('Feed article fetch completed', null, Mockery::type('array')) ->once(); @@ -219,4 +219,4 @@ protected function tearDown(): void Mockery::close(); parent::tearDown(); } -} \ No newline at end of file +} diff --git a/tests/Unit/Jobs/ArticleDiscoveryJobTest.php b/tests/Unit/Jobs/ArticleDiscoveryJobTest.php index 4db26ae..d9546d0 100644 --- a/tests/Unit/Jobs/ArticleDiscoveryJobTest.php +++ b/tests/Unit/Jobs/ArticleDiscoveryJobTest.php @@ -23,7 +23,7 @@ protected function setUp(): void public function test_constructor_sets_correct_queue(): void { // Act - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Assert $this->assertEquals('feed-discovery', $job->queue); @@ -40,7 +40,7 @@ public function test_handle_skips_when_article_processing_disabled(): void ->once() ->with('Article processing is disabled. Article discovery skipped.'); - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Act $job->handle($logSaverMock); @@ -63,7 +63,7 @@ public function test_handle_dispatches_jobs_when_article_processing_enabled(): v ->with('Article discovery jobs dispatched for all active feeds') ->once(); - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Act $job->handle($logSaverMock); @@ -85,7 +85,7 @@ public function test_handle_with_default_article_processing_enabled(): void ->with('Article discovery jobs dispatched for all active feeds') ->once(); - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Act $job->handle($logSaverMock); @@ -97,7 +97,7 @@ public function test_handle_with_default_article_processing_enabled(): void public function test_job_implements_should_queue(): void { // Arrange - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Assert $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); @@ -106,7 +106,7 @@ public function test_job_implements_should_queue(): void public function test_job_uses_queueable_trait(): void { // Arrange - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Assert $this->assertTrue(method_exists($job, 'onQueue')); @@ -129,7 +129,7 @@ public function test_handle_logs_appropriate_messages(): void ->with('Article discovery jobs dispatched for all active feeds') ->once(); - $job = new ArticleDiscoveryJob(); + $job = new ArticleDiscoveryJob; // Act - Should not throw any exceptions $job->handle($logSaverMock); diff --git a/tests/Unit/Jobs/PublishNextArticleJobTest.php b/tests/Unit/Jobs/PublishNextArticleJobTest.php index af12d93..46cdf3c 100644 --- a/tests/Unit/Jobs/PublishNextArticleJobTest.php +++ b/tests/Unit/Jobs/PublishNextArticleJobTest.php @@ -25,36 +25,36 @@ protected function setUp(): void public function test_constructor_sets_correct_queue(): void { - $job = new PublishNextArticleJob(); - + $job = new PublishNextArticleJob; + $this->assertEquals('publishing', $job->queue); } public function test_job_implements_should_queue(): void { - $job = new PublishNextArticleJob(); - + $job = new PublishNextArticleJob; + $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); } public function test_job_implements_should_be_unique(): void { - $job = new PublishNextArticleJob(); - + $job = new PublishNextArticleJob; + $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job); } public function test_job_has_unique_for_property(): void { - $job = new PublishNextArticleJob(); - + $job = new PublishNextArticleJob; + $this->assertEquals(300, $job->uniqueFor); } public function test_job_uses_queueable_trait(): void { - $job = new PublishNextArticleJob(); - + $job = new PublishNextArticleJob; + $this->assertContains( \Illuminate\Foundation\Queue\Queueable::class, class_uses($job) @@ -66,8 +66,8 @@ public function test_handle_returns_early_when_no_approved_articles(): void // Arrange - No articles exist $articleFetcherMock = Mockery::mock(ArticleFetcher::class); // No expectations as handle should return early - - $job = new PublishNextArticleJob(); + + $job = new PublishNextArticleJob; // Act $publishingServiceMock = \Mockery::mock(ArticlePublishingService::class); @@ -83,16 +83,16 @@ public function test_handle_returns_early_when_no_unpublished_approved_articles( $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); - + // Create a publication record to mark it as already published ArticlePublication::factory()->create(['article_id' => $article->id]); $articleFetcherMock = Mockery::mock(ArticleFetcher::class); // No expectations as handle should return early - - $job = new PublishNextArticleJob(); + + $job = new PublishNextArticleJob; // Act $publishingServiceMock = \Mockery::mock(ArticlePublishingService::class); @@ -108,17 +108,17 @@ public function test_handle_skips_non_approved_articles(): void $feed = Feed::factory()->create(); Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'rejected' + 'approval_status' => 'rejected', ]); $articleFetcherMock = Mockery::mock(ArticleFetcher::class); // No expectations as handle should return early - - $job = new PublishNextArticleJob(); + + $job = new PublishNextArticleJob; // Act $publishingServiceMock = \Mockery::mock(ArticlePublishingService::class); @@ -132,19 +132,19 @@ public function test_handle_publishes_oldest_approved_article(): void { // Arrange $feed = Feed::factory()->create(); - + // Create older article first $olderArticle = Article::factory()->create([ 'feed_id' => $feed->id, 'approval_status' => 'approved', - 'created_at' => now()->subHours(2) + 'created_at' => now()->subHours(2), ]); - + // Create newer article $newerArticle = Article::factory()->create([ 'feed_id' => $feed->id, 'approval_status' => 'approved', - 'created_at' => now()->subHour() + 'created_at' => now()->subHour(), ]); $extractedData = ['title' => 'Test Article', 'content' => 'Test content']; @@ -169,7 +169,7 @@ public function test_handle_publishes_oldest_approved_article(): void $extractedData ); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; // Act $job->handle($articleFetcherMock, $publishingServiceMock); @@ -184,7 +184,7 @@ public function test_handle_throws_exception_on_publishing_failure(): void $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = ['title' => 'Test Article']; @@ -203,7 +203,7 @@ public function test_handle_throws_exception_on_publishing_failure(): void ->once() ->andThrow($publishException); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; // Assert $this->expectException(PublishException::class); @@ -220,7 +220,7 @@ public function test_handle_logs_publishing_start(): void 'feed_id' => $feed->id, 'approval_status' => 'approved', 'title' => 'Test Article Title', - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $extractedData = ['title' => 'Test Article']; @@ -235,7 +235,7 @@ public function test_handle_logs_publishing_start(): void $publishingServiceMock = Mockery::mock(ArticlePublishingService::class); $publishingServiceMock->shouldReceive('publishToRoutedChannels')->once(); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; // Act $job->handle($articleFetcherMock, $publishingServiceMock); @@ -246,11 +246,11 @@ public function test_handle_logs_publishing_start(): void public function test_job_can_be_serialized(): void { - $job = new PublishNextArticleJob(); - + $job = new PublishNextArticleJob; + $serialized = serialize($job); $unserialized = unserialize($serialized); - + $this->assertInstanceOf(PublishNextArticleJob::class, $unserialized); $this->assertEquals($job->queue, $unserialized->queue); $this->assertEquals($job->uniqueFor, $unserialized->uniqueFor); @@ -262,7 +262,7 @@ public function test_handle_fetches_article_data_before_publishing(): void $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = ['title' => 'Extracted Title', 'content' => 'Extracted Content']; @@ -280,7 +280,7 @@ public function test_handle_fetches_article_data_before_publishing(): void ->once() ->with(Mockery::type(Article::class), $extractedData); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; // Act $job->handle($articleFetcherMock, $publishingServiceMock); @@ -310,7 +310,7 @@ public function test_handle_skips_publishing_when_last_publication_within_interv $articleFetcherMock->shouldNotReceive('fetchArticleData'); $publishingServiceMock->shouldNotReceive('publishToRoutedChannels'); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; $job->handle($articleFetcherMock, $publishingServiceMock); $this->assertTrue(true); @@ -341,7 +341,7 @@ public function test_handle_publishes_when_last_publication_beyond_interval(): v $publishingServiceMock->shouldReceive('publishToRoutedChannels') ->once(); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; $job->handle($articleFetcherMock, $publishingServiceMock); $this->assertTrue(true); @@ -372,7 +372,7 @@ public function test_handle_publishes_when_interval_is_zero(): void $publishingServiceMock->shouldReceive('publishToRoutedChannels') ->once(); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; $job->handle($articleFetcherMock, $publishingServiceMock); $this->assertTrue(true); @@ -403,7 +403,7 @@ public function test_handle_publishes_when_last_publication_exactly_at_interval( $publishingServiceMock->shouldReceive('publishToRoutedChannels') ->once(); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; $job->handle($articleFetcherMock, $publishingServiceMock); $this->assertTrue(true); @@ -430,7 +430,7 @@ public function test_handle_publishes_when_no_previous_publications_exist(): voi $publishingServiceMock->shouldReceive('publishToRoutedChannels') ->once(); - $job = new PublishNextArticleJob(); + $job = new PublishNextArticleJob; $job->handle($articleFetcherMock, $publishingServiceMock); $this->assertTrue(true); @@ -441,4 +441,4 @@ protected function tearDown(): void Mockery::close(); parent::tearDown(); } -} \ No newline at end of file +} diff --git a/tests/Unit/Jobs/SyncChannelPostsJobTest.php b/tests/Unit/Jobs/SyncChannelPostsJobTest.php index 6b10a61..7286a55 100644 --- a/tests/Unit/Jobs/SyncChannelPostsJobTest.php +++ b/tests/Unit/Jobs/SyncChannelPostsJobTest.php @@ -3,12 +3,10 @@ namespace Tests\Unit\Jobs; use App\Enums\PlatformEnum; -use App\Exceptions\PlatformAuthException; use App\Jobs\SyncChannelPostsJob; use App\Models\PlatformAccount; use App\Models\PlatformChannel; use App\Models\PlatformInstance; -use App\Modules\Lemmy\Services\LemmyApiService; use App\Services\Log\LogSaver; use Exception; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -32,7 +30,7 @@ public function test_constructor_sets_correct_queue(): void { $channel = PlatformChannel::factory()->make(); $job = new SyncChannelPostsJob($channel); - + $this->assertEquals('sync', $job->queue); } @@ -40,7 +38,7 @@ public function test_job_implements_should_queue(): void { $channel = PlatformChannel::factory()->make(); $job = new SyncChannelPostsJob($channel); - + $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); } @@ -48,7 +46,7 @@ public function test_job_implements_should_be_unique(): void { $channel = PlatformChannel::factory()->make(); $job = new SyncChannelPostsJob($channel); - + $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job); } @@ -56,7 +54,7 @@ public function test_job_uses_queueable_trait(): void { $channel = PlatformChannel::factory()->make(); $job = new SyncChannelPostsJob($channel); - + $this->assertContains( \Illuminate\Foundation\Queue\Queueable::class, class_uses($job) @@ -67,30 +65,30 @@ public function test_dispatch_for_all_active_channels_dispatches_jobs(): void { // Arrange $platformInstance = PlatformInstance::factory()->create([ - 'platform' => PlatformEnum::LEMMY + 'platform' => PlatformEnum::LEMMY, ]); - + $account = PlatformAccount::factory()->create([ 'instance_url' => $platformInstance->url, - 'is_active' => true + 'is_active' => true, ]); - + $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $platformInstance->id, - 'is_active' => true + 'is_active' => true, ]); - + // Attach account to channel with active status $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, 'created_at' => now(), - 'updated_at' => now() + 'updated_at' => now(), ]); // Mock LogSaver to avoid strict expectations $logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock->shouldReceive('info')->zeroOrMoreTimes(); - + $this->app->instance(LogSaver::class, $logSaverMock); // Act @@ -105,12 +103,12 @@ public function test_handle_logs_start_message(): void // Arrange $platformInstance = PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'url' => 'https://lemmy.example.com' + 'url' => 'https://lemmy.example.com', ]); - + $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $platformInstance->id, - 'name' => 'testcommunity' + 'name' => 'testcommunity', ]); // Mock LogSaver - only test that logging methods are called @@ -136,13 +134,13 @@ public function test_job_can_be_serialized(): void $platformInstance = PlatformInstance::factory()->create(); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $platformInstance->id, - 'name' => 'Test Channel' + 'name' => 'Test Channel', ]); $job = new SyncChannelPostsJob($channel); - + $serialized = serialize($job); $unserialized = unserialize($serialized); - + $this->assertInstanceOf(SyncChannelPostsJob::class, $unserialized); $this->assertEquals($job->queue, $unserialized->queue); // Note: Cannot test channel property directly as it's private @@ -158,7 +156,7 @@ public function test_job_has_handle_method(): void { $channel = PlatformChannel::factory()->make(); $job = new SyncChannelPostsJob($channel); - + $this->assertTrue(method_exists($job, 'handle')); } @@ -167,4 +165,4 @@ protected function tearDown(): void Mockery::close(); parent::tearDown(); } -} \ No newline at end of file +} diff --git a/tests/Unit/Listeners/LogActionListenerTest.php b/tests/Unit/Listeners/LogActionListenerTest.php new file mode 100644 index 0000000..fd9e291 --- /dev/null +++ b/tests/Unit/Listeners/LogActionListenerTest.php @@ -0,0 +1,85 @@ +<?php + +namespace Tests\Unit\Listeners; + +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; +use App\Listeners\LogActionListener; +use App\Models\Log; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +class LogActionListenerTest extends TestCase +{ + use RefreshDatabase; + + public function test_listener_creates_log_entry_with_correct_data(): void + { + $event = new ActionPerformed( + 'Article published successfully', + LogLevelEnum::INFO, + ['article_id' => 42], + ); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'info', + 'message' => 'Article published successfully', + ]); + + $log = Log::where('message', 'Article published successfully')->first(); + $this->assertEquals(['article_id' => 42], $log->context); + } + + public function test_listener_creates_log_with_warning_level(): void + { + $event = new ActionPerformed( + 'No publications created', + LogLevelEnum::WARNING, + ['article_id' => 7], + ); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'warning', + 'message' => 'No publications created', + ]); + } + + public function test_listener_creates_log_with_error_level(): void + { + $event = new ActionPerformed( + 'Publishing failed', + LogLevelEnum::ERROR, + ['error' => 'Connection timeout'], + ); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'error', + 'message' => 'Publishing failed', + ]); + } + + public function test_listener_creates_log_with_empty_context(): void + { + $event = new ActionPerformed('Simple action'); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'info', + 'message' => 'Simple action', + ]); + + $log = Log::where('message', 'Simple action')->first(); + $this->assertEquals([], $log->context); + } +} diff --git a/tests/Unit/Models/ArticlePublicationTest.php b/tests/Unit/Models/ArticlePublicationTest.php index 0a0c1de..3c7e69c 100644 --- a/tests/Unit/Models/ArticlePublicationTest.php +++ b/tests/Unit/Models/ArticlePublicationTest.php @@ -15,21 +15,22 @@ class ArticlePublicationTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['article_id', 'platform_channel_id', 'post_id', 'published_at', 'published_by', 'platform', 'publication_data']; - $publication = new ArticlePublication(); - + $publication = new ArticlePublication; + $this->assertEquals($fillableFields, $publication->getFillable()); } public function test_table_name(): void { - $publication = new ArticlePublication(); - + $publication = new ArticlePublication; + $this->assertEquals('article_publications', $publication->getTable()); } public function test_casts_published_at_to_datetime(): void { $timestamp = now()->subHours(2); + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['published_at' => $timestamp]); $this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at); @@ -43,11 +44,12 @@ public function test_casts_publication_data_to_array(): void 'platform_response' => [ 'id' => 123, 'status' => 'success', - 'metadata' => ['views' => 0, 'votes' => 0] + 'metadata' => ['views' => 0, 'votes' => 0], ], - 'retry_count' => 0 + 'retry_count' => 0, ]; - + + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['publication_data' => $publicationData]); $this->assertIsArray($publication->publication_data); @@ -57,6 +59,7 @@ public function test_casts_publication_data_to_array(): void public function test_belongs_to_article_relationship(): void { $article = Article::factory()->create(); + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['article_id' => $article->id]); $this->assertInstanceOf(Article::class, $publication->article); @@ -66,6 +69,7 @@ public function test_belongs_to_article_relationship(): void public function test_publication_creation_with_factory(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(); $this->assertInstanceOf(ArticlePublication::class, $publication); @@ -90,7 +94,7 @@ public function test_publication_creation_with_explicit_values(): void 'published_at' => $publishedAt, 'published_by' => 'test_bot', 'platform' => 'lemmy', - 'publication_data' => $publicationData + 'publication_data' => $publicationData, ]); $this->assertEquals($article->id, $publication->article_id); @@ -104,8 +108,9 @@ public function test_publication_creation_with_explicit_values(): void public function test_publication_factory_recently_published_state(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->recentlyPublished()->create(); - + $this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at); $this->assertTrue($publication->published_at->isAfter(now()->subDay())); $this->assertTrue($publication->published_at->isBefore(now()->addMinute())); @@ -113,14 +118,15 @@ public function test_publication_factory_recently_published_state(): void public function test_publication_update(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create([ 'post_id' => 'original-id', - 'published_by' => 'original_user' + 'published_by' => 'original_user', ]); $publication->update([ 'post_id' => 'updated-id', - 'published_by' => 'updated_user' + 'published_by' => 'updated_user', ]); $publication->refresh(); @@ -131,6 +137,7 @@ public function test_publication_update(): void public function test_publication_deletion(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(); $publicationId = $publication->id; @@ -141,6 +148,7 @@ public function test_publication_deletion(): void public function test_publication_data_can_be_empty_array(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['publication_data' => []]); $this->assertIsArray($publication->publication_data); @@ -149,6 +157,7 @@ public function test_publication_data_can_be_empty_array(): void public function test_publication_data_can_be_null(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['publication_data' => null]); $this->assertNull($publication->publication_data); @@ -164,21 +173,22 @@ public function test_publication_data_can_be_complex_structure(): void 'author' => [ 'id' => 456, 'name' => 'bot_user', - 'display_name' => 'Bot User' - ] + 'display_name' => 'Bot User', + ], ], 'metadata' => [ 'retry_attempts' => 1, 'processing_time_ms' => 1250, - 'error_log' => [] + 'error_log' => [], ], 'analytics' => [ 'initial_views' => 0, 'initial_votes' => 0, - 'engagement_tracked' => false - ] + 'engagement_tracked' => false, + ], ]; + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['publication_data' => $complexData]); $this->assertEquals($complexData, $publication->publication_data); @@ -190,6 +200,7 @@ public function test_publication_data_can_be_complex_structure(): void public function test_publication_with_specific_published_at(): void { $timestamp = now()->subHours(3); + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['published_at' => $timestamp]); $this->assertInstanceOf(\Carbon\Carbon::class, $publication->published_at); @@ -198,6 +209,7 @@ public function test_publication_with_specific_published_at(): void public function test_publication_with_specific_published_by(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['published_by' => 'custom_bot']); $this->assertEquals('custom_bot', $publication->published_by); @@ -205,6 +217,7 @@ public function test_publication_with_specific_published_by(): void public function test_publication_with_specific_platform(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['platform' => 'lemmy']); $this->assertEquals('lemmy', $publication->platform); @@ -212,6 +225,7 @@ public function test_publication_with_specific_platform(): void public function test_publication_timestamps(): void { + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(); $this->assertNotNull($publication->created_at); @@ -226,16 +240,18 @@ public function test_multiple_publications_for_same_article(): void $channel1 = PlatformChannel::factory()->create(); $channel2 = PlatformChannel::factory()->create(); + /** @var ArticlePublication $publication1 */ $publication1 = ArticlePublication::factory()->create([ 'article_id' => $article->id, 'platform_channel_id' => $channel1->id, - 'post_id' => 'post-1' + 'post_id' => 'post-1', ]); + /** @var ArticlePublication $publication2 */ $publication2 = ArticlePublication::factory()->create([ 'article_id' => $article->id, 'platform_channel_id' => $channel2->id, - 'post_id' => 'post-2' + 'post_id' => 'post-2', ]); $this->assertEquals($article->id, $publication1->article_id); @@ -246,7 +262,9 @@ public function test_multiple_publications_for_same_article(): void public function test_publication_with_different_platforms(): void { + /** @var ArticlePublication $publication1 */ $publication1 = ArticlePublication::factory()->create(['platform' => 'lemmy']); + /** @var ArticlePublication $publication2 */ $publication2 = ArticlePublication::factory()->create(['platform' => 'lemmy']); $this->assertEquals('lemmy', $publication1->platform); @@ -255,9 +273,10 @@ public function test_publication_with_different_platforms(): void public function test_publication_post_id_variations(): void { + /** @var ArticlePublication[] $publications */ $publications = [ ArticlePublication::factory()->create(['post_id' => 'numeric-123']), - ArticlePublication::factory()->create(['post_id' => 'uuid-' . fake()->uuid()]), + ArticlePublication::factory()->create(['post_id' => 'uuid-'.fake()->uuid()]), ArticlePublication::factory()->create(['post_id' => 'alphanumeric_post_456']), ArticlePublication::factory()->create(['post_id' => '12345']), ]; @@ -275,15 +294,16 @@ public function test_publication_data_with_error_information(): void 'error' => [ 'code' => 403, 'message' => 'Insufficient permissions', - 'details' => 'Bot account lacks posting privileges' + 'details' => 'Bot account lacks posting privileges', ], 'retry_info' => [ 'max_retries' => 3, 'current_attempt' => 2, - 'next_retry_at' => '2023-01-01T13:00:00Z' - ] + 'next_retry_at' => '2023-01-01T13:00:00Z', + ], ]; + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['publication_data' => $errorData]); $this->assertEquals('failed', $publication->publication_data['status']); @@ -295,12 +315,13 @@ public function test_publication_relationship_with_article_data(): void { $article = Article::factory()->create([ 'title' => 'Test Article Title', - 'description' => 'Test article description' + 'description' => 'Test article description', ]); - + + /** @var ArticlePublication $publication */ $publication = ArticlePublication::factory()->create(['article_id' => $article->id]); $this->assertEquals('Test Article Title', $publication->article->title); $this->assertEquals('Test article description', $publication->article->description); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/ArticleTest.php b/tests/Unit/Models/ArticleTest.php index 93d5617..ed9cd0d 100644 --- a/tests/Unit/Models/ArticleTest.php +++ b/tests/Unit/Models/ArticleTest.php @@ -22,7 +22,7 @@ protected function setUp(): void // Mock HTTP requests to prevent external calls Http::fake([ - '*' => Http::response('', 500) + '*' => Http::response('', 500), ]); // Don't fake events globally - let individual tests control this diff --git a/tests/Unit/Models/FeedTest.php b/tests/Unit/Models/FeedTest.php index 94beef8..6c4029f 100644 --- a/tests/Unit/Models/FeedTest.php +++ b/tests/Unit/Models/FeedTest.php @@ -17,15 +17,15 @@ class FeedTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at']; - $feed = new Feed(); - + $feed = new Feed; + $this->assertEquals($fillableFields, $feed->getFillable()); } public function test_casts_settings_to_array(): void { $settings = ['key1' => 'value1', 'key2' => ['nested' => 'value']]; - + $feed = Feed::factory()->create(['settings' => $settings]); $this->assertIsArray($feed->settings); @@ -41,7 +41,7 @@ public function test_casts_is_active_to_boolean(): void $feed->update(['is_active' => '0']); $feed->refresh(); - + $this->assertIsBool($feed->is_active); $this->assertFalse($feed->is_active); } @@ -75,7 +75,7 @@ public function test_status_attribute_never_fetched(): void { $feed = Feed::factory()->create([ 'is_active' => true, - 'last_fetched_at' => null + 'last_fetched_at' => null, ]); $this->assertEquals('Never fetched', $feed->status); @@ -85,7 +85,7 @@ public function test_status_attribute_recently_fetched(): void { $feed = Feed::factory()->create([ 'is_active' => true, - 'last_fetched_at' => now()->subHour() + 'last_fetched_at' => now()->subHour(), ]); $this->assertEquals('Recently fetched', $feed->status); @@ -95,7 +95,7 @@ public function test_status_attribute_fetched_hours_ago(): void { $feed = Feed::factory()->create([ 'is_active' => true, - 'last_fetched_at' => now()->subHours(5)->startOfHour() + 'last_fetched_at' => now()->subHours(5)->startOfHour(), ]); $this->assertStringContainsString('Fetched', $feed->status); @@ -106,7 +106,7 @@ public function test_status_attribute_fetched_days_ago(): void { $feed = Feed::factory()->create([ 'is_active' => true, - 'last_fetched_at' => now()->subDays(3) + 'last_fetched_at' => now()->subDays(3), ]); $this->assertStringStartsWith('Fetched', $feed->status); @@ -126,10 +126,10 @@ public function test_belongs_to_language_relationship(): void public function test_has_many_articles_relationship(): void { $feed = Feed::factory()->create(); - + $article1 = Article::factory()->create(['feed_id' => $feed->id]); $article2 = Article::factory()->create(['feed_id' => $feed->id]); - + // Create article for different feed $otherFeed = Feed::factory()->create(); Article::factory()->create(['feed_id' => $otherFeed->id]); @@ -153,14 +153,14 @@ public function test_belongs_to_many_channels_relationship(): void 'feed_id' => $feed->id, 'platform_channel_id' => $channel1->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel2->id, 'is_active' => false, - 'priority' => 50 + 'priority' => 50, ]); $channels = $feed->channels; @@ -187,21 +187,21 @@ public function test_active_channels_relationship(): void 'feed_id' => $feed->id, 'platform_channel_id' => $activeChannel1->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $activeChannel2->id, 'is_active' => true, - 'priority' => 200 + 'priority' => 200, ]); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $inactiveChannel->id, 'is_active' => false, - 'priority' => 150 + 'priority' => 150, ]); $activeChannels = $feed->activeChannels; @@ -225,8 +225,7 @@ public function test_feed_creation_with_factory(): void $this->assertIsString($feed->name); $this->assertIsString($feed->url); $this->assertIsString($feed->type); - // Language ID may be null as it's nullable in the database - $this->assertTrue($feed->language_id === null || is_int($feed->language_id)); + $this->assertNull($feed->language_id); $this->assertIsBool($feed->is_active); $this->assertIsArray($feed->settings); } @@ -244,7 +243,7 @@ public function test_feed_creation_with_explicit_values(): void 'language_id' => $language->id, 'description' => 'Test description', 'settings' => $settings, - 'is_active' => false + 'is_active' => false, ]); $this->assertEquals('Test Feed', $feed->name); @@ -260,12 +259,12 @@ public function test_feed_update(): void { $feed = Feed::factory()->create([ 'name' => 'Original Name', - 'is_active' => true + 'is_active' => true, ]); $feed->update([ 'name' => 'Updated Name', - 'is_active' => false + 'is_active' => false, ]); $feed->refresh(); @@ -298,13 +297,13 @@ public function test_feed_settings_can_be_complex_structure(): void 'parsing' => [ 'selector' => 'article.post', 'title_selector' => 'h1', - 'content_selector' => '.content' + 'content_selector' => '.content', ], 'filters' => ['min_length' => 100], 'schedule' => [ 'enabled' => true, - 'interval' => 3600 - ] + 'interval' => 3600, + ], ]; $feed = Feed::factory()->create(['settings' => $complexSettings]); @@ -330,4 +329,4 @@ public function test_feed_timestamps(): void $this->assertInstanceOf(\Carbon\Carbon::class, $feed->created_at); $this->assertInstanceOf(\Carbon\Carbon::class, $feed->updated_at); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/KeywordTest.php b/tests/Unit/Models/KeywordTest.php index e8b4208..d1fbd13 100644 --- a/tests/Unit/Models/KeywordTest.php +++ b/tests/Unit/Models/KeywordTest.php @@ -15,8 +15,8 @@ class KeywordTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['feed_id', 'platform_channel_id', 'keyword', 'is_active']; - $keyword = new Keyword(); - + $keyword = new Keyword; + $this->assertEquals($fillableFields, $keyword->getFillable()); } @@ -24,12 +24,12 @@ public function test_casts_is_active_to_boolean(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $keyword = Keyword::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'test', - 'is_active' => '1' + 'is_active' => '1', ]); $this->assertIsBool($keyword->is_active); @@ -37,7 +37,7 @@ public function test_casts_is_active_to_boolean(): void $keyword->update(['is_active' => '0']); $keyword->refresh(); - + $this->assertIsBool($keyword->is_active); $this->assertFalse($keyword->is_active); } @@ -46,12 +46,12 @@ public function test_belongs_to_feed_relationship(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $keyword = Keyword::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'test keyword', - 'is_active' => true + 'is_active' => true, ]); $this->assertInstanceOf(Feed::class, $keyword->feed); @@ -63,12 +63,12 @@ public function test_belongs_to_platform_channel_relationship(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $keyword = Keyword::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'test keyword', - 'is_active' => true + 'is_active' => true, ]); $this->assertInstanceOf(PlatformChannel::class, $keyword->platformChannel); @@ -96,7 +96,7 @@ public function test_keyword_creation_with_explicit_values(): void 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'Belgium', - 'is_active' => false + 'is_active' => false, ]); $this->assertEquals($feed->id, $keyword->feed_id); @@ -107,14 +107,15 @@ public function test_keyword_creation_with_explicit_values(): void public function test_keyword_update(): void { + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create([ 'keyword' => 'original', - 'is_active' => true + 'is_active' => true, ]); $keyword->update([ 'keyword' => 'updated', - 'is_active' => false + 'is_active' => false, ]); $keyword->refresh(); @@ -125,6 +126,7 @@ public function test_keyword_update(): void public function test_keyword_deletion(): void { + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create(); $keywordId = $keyword->id; @@ -145,7 +147,7 @@ public function test_keyword_with_special_characters(): void 'keyword with spaces', 'UPPERCASE', 'lowercase', - 'MixedCase' + 'MixedCase', ]; foreach ($specialKeywords as $keywordText) { @@ -153,14 +155,14 @@ public function test_keyword_with_special_characters(): void 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => $keywordText, - 'is_active' => true + 'is_active' => true, ]); $this->assertEquals($keywordText, $keyword->keyword); $this->assertDatabaseHas('keywords', [ 'keyword' => $keywordText, 'feed_id' => $feed->id, - 'platform_channel_id' => $channel->id + 'platform_channel_id' => $channel->id, ]); } } @@ -174,26 +176,26 @@ public function test_multiple_keywords_for_same_route(): void 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'keyword1', - 'is_active' => true + 'is_active' => true, ]); $keyword2 = Keyword::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'keyword2', - 'is_active' => false + 'is_active' => false, ]); $this->assertDatabaseHas('keywords', [ 'id' => $keyword1->id, 'keyword' => 'keyword1', - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('keywords', [ 'id' => $keyword2->id, 'keyword' => 'keyword2', - 'is_active' => false + 'is_active' => false, ]); } @@ -207,17 +209,17 @@ public function test_keyword_uniqueness_constraint(): void 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'unique_keyword', - 'is_active' => true + 'is_active' => true, ]); // Attempt to create duplicate should fail $this->expectException(\Illuminate\Database\QueryException::class); - + Keyword::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'unique_keyword', - 'is_active' => true + 'is_active' => true, ]); } @@ -233,14 +235,14 @@ public function test_same_keyword_different_routes_allowed(): void 'feed_id' => $feed1->id, 'platform_channel_id' => $channel1->id, 'keyword' => 'common_keyword', - 'is_active' => true + 'is_active' => true, ]); $keyword2 = Keyword::create([ 'feed_id' => $feed2->id, 'platform_channel_id' => $channel2->id, 'keyword' => 'common_keyword', - 'is_active' => true + 'is_active' => true, ]); $this->assertDatabaseHas('keywords', ['id' => $keyword1->id]); @@ -250,6 +252,7 @@ public function test_same_keyword_different_routes_allowed(): void public function test_keyword_timestamps(): void { + /** @var Keyword $keyword */ $keyword = Keyword::factory()->create(); $this->assertNotNull($keyword->created_at); @@ -267,7 +270,7 @@ public function test_keyword_default_active_state(): void $keyword = Keyword::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'keyword' => 'test' + 'keyword' => 'test', ]); // Refresh to get the actual database values including defaults @@ -277,4 +280,4 @@ public function test_keyword_default_active_state(): void $this->assertIsBool($keyword->is_active); $this->assertTrue($keyword->is_active); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/LanguageTest.php b/tests/Unit/Models/LanguageTest.php index f0a768f..3017be0 100644 --- a/tests/Unit/Models/LanguageTest.php +++ b/tests/Unit/Models/LanguageTest.php @@ -16,15 +16,15 @@ class LanguageTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['short_code', 'name', 'native_name', 'is_active']; - $language = new Language(); - + $language = new Language; + $this->assertEquals($fillableFields, $language->getFillable()); } public function test_table_name(): void { - $language = new Language(); - + $language = new Language; + $this->assertEquals('languages', $language->getTable()); } @@ -37,7 +37,7 @@ public function test_casts_is_active_to_boolean(): void $language->update(['is_active' => '0']); $language->refresh(); - + $this->assertIsBool($language->is_active); $this->assertFalse($language->is_active); } @@ -51,7 +51,7 @@ public function test_belongs_to_many_platform_instances_relationship(): void // Attach with required platform_language_id $language->platformInstances()->attach([ $instance1->id => ['platform_language_id' => 1], - $instance2->id => ['platform_language_id' => 2] + $instance2->id => ['platform_language_id' => 2], ]); $instances = $language->platformInstances; @@ -64,10 +64,10 @@ public function test_belongs_to_many_platform_instances_relationship(): void public function test_has_many_platform_channels_relationship(): void { $language = Language::factory()->create(); - + $channel1 = PlatformChannel::factory()->create(['language_id' => $language->id]); $channel2 = PlatformChannel::factory()->create(['language_id' => $language->id]); - + // Create channel for different language $otherLanguage = Language::factory()->create(); PlatformChannel::factory()->create(['language_id' => $otherLanguage->id]); @@ -83,10 +83,10 @@ public function test_has_many_platform_channels_relationship(): void public function test_has_many_feeds_relationship(): void { $language = Language::factory()->create(); - + $feed1 = Feed::factory()->create(['language_id' => $language->id]); $feed2 = Feed::factory()->create(['language_id' => $language->id]); - + // Create feed for different language $otherLanguage = Language::factory()->create(); Feed::factory()->create(['language_id' => $otherLanguage->id]); @@ -115,7 +115,7 @@ public function test_language_creation_with_explicit_values(): void 'short_code' => 'fr', 'name' => 'French', 'native_name' => 'Français', - 'is_active' => false + 'is_active' => false, ]); $this->assertEquals('fr', $language->short_code); @@ -139,12 +139,12 @@ public function test_language_update(): void { $language = Language::factory()->create([ 'name' => 'Original Name', - 'is_active' => true + 'is_active' => true, ]); $language->update([ 'name' => 'Updated Name', - 'is_active' => false + 'is_active' => false, ]); $language->refresh(); @@ -180,7 +180,7 @@ public function test_language_can_have_empty_native_name(): void public function test_language_short_code_variations(): void { $shortCodes = ['en', 'fr', 'es', 'de', 'zh', 'pt', 'nl', 'it']; - + foreach ($shortCodes as $code) { $language = Language::factory()->create(['short_code' => $code]); $this->assertEquals($code, $language->short_code); @@ -208,7 +208,7 @@ public function test_language_can_have_multiple_platform_instances(): void $language->platformInstances()->attach([ $instance1->id => ['platform_language_id' => 1], $instance2->id => ['platform_language_id' => 2], - $instance3->id => ['platform_language_id' => 3] + $instance3->id => ['platform_language_id' => 3], ]); $instances = $language->platformInstances; @@ -245,13 +245,13 @@ public function test_multiple_languages_with_same_name_different_regions(): void $englishUS = Language::factory()->create([ 'short_code' => 'en-US', 'name' => 'English (United States)', - 'native_name' => 'English' + 'native_name' => 'English', ]); $englishGB = Language::factory()->create([ 'short_code' => 'en-GB', 'name' => 'English (United Kingdom)', - 'native_name' => 'English' + 'native_name' => 'English', ]); $this->assertEquals('English', $englishUS->native_name); @@ -272,7 +272,7 @@ public function test_language_with_complex_native_name(): void foreach ($complexLanguages as $langData) { $language = Language::factory()->create($langData); - + $this->assertEquals($langData['short_code'], $language->short_code); $this->assertEquals($langData['name'], $language->name); $this->assertEquals($langData['native_name'], $language->native_name); @@ -291,23 +291,23 @@ public function test_language_active_and_inactive_states(): void public function test_language_relationships_maintain_referential_integrity(): void { $language = Language::factory()->create(); - + // Create related models $instance = PlatformInstance::factory()->create(); $channel = PlatformChannel::factory()->create(['language_id' => $language->id]); $feed = Feed::factory()->create(['language_id' => $language->id]); - + // Attach instance $language->platformInstances()->attach($instance->id, [ 'platform_language_id' => 1, - 'is_default' => true + 'is_default' => true, ]); // Verify all relationships work $this->assertCount(1, $language->platformInstances); $this->assertCount(1, $language->platformChannels); $this->assertCount(1, $language->feeds); - + $this->assertEquals($language->id, $channel->language_id); $this->assertEquals($language->id, $feed->language_id); } @@ -317,8 +317,8 @@ public function test_language_factory_unique_constraints(): void // The factory should generate unique short codes $language1 = Language::factory()->create(); $language2 = Language::factory()->create(); - + $this->assertNotEquals($language1->short_code, $language2->short_code); $this->assertNotEquals($language1->name, $language2->name); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/PlatformAccountTest.php b/tests/Unit/Models/PlatformAccountTest.php index 7e71501..c3e351e 100644 --- a/tests/Unit/Models/PlatformAccountTest.php +++ b/tests/Unit/Models/PlatformAccountTest.php @@ -6,7 +6,6 @@ use App\Models\PlatformAccount; use App\Models\PlatformChannel; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Crypt; use Tests\TestCase; class PlatformAccountTest extends TestCase @@ -16,15 +15,15 @@ class PlatformAccountTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['platform', 'instance_url', 'username', 'password', 'settings', 'is_active', 'last_tested_at', 'status']; - $account = new PlatformAccount(); - + $account = new PlatformAccount; + $this->assertEquals($fillableFields, $account->getFillable()); } public function test_table_name(): void { - $account = new PlatformAccount(); - + $account = new PlatformAccount; + $this->assertEquals('platform_accounts', $account->getTable()); } @@ -40,7 +39,7 @@ public function test_casts_platform_to_enum(): void public function test_casts_settings_to_array(): void { $settings = ['key1' => 'value1', 'nested' => ['key2' => 'value2']]; - + $account = PlatformAccount::factory()->create(['settings' => $settings]); $this->assertIsArray($account->settings); @@ -56,7 +55,7 @@ public function test_casts_is_active_to_boolean(): void $account->update(['is_active' => '0']); $account->refresh(); - + $this->assertIsBool($account->is_active); $this->assertFalse($account->is_active); } @@ -73,12 +72,12 @@ public function test_casts_last_tested_at_to_datetime(): void public function test_password_encryption_and_decryption(): void { $plainPassword = 'my-secret-password'; - + $account = PlatformAccount::factory()->create(['password' => $plainPassword]); // Password should be decrypted when accessing $this->assertEquals($plainPassword, $account->password); - + // But encrypted in the database $this->assertNotEquals($plainPassword, $account->getAttributes()['password']); $this->assertNotNull($account->getAttributes()['password']); @@ -104,12 +103,11 @@ public function test_password_encryption_is_different_each_time(): void $this->assertNotEquals($account1->getAttributes()['password'], $account2->getAttributes()['password']); } - public function test_password_decryption_handles_corruption(): void { $account = PlatformAccount::factory()->create(); $originalPassword = $account->password; - + // Since the password attribute has special handling, this test verifies the basic functionality $this->assertNotNull($originalPassword); $this->assertIsString($originalPassword); @@ -120,17 +118,17 @@ public function test_get_active_static_method(): void // Create active and inactive accounts $activeAccount1 = PlatformAccount::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'is_active' => true + 'is_active' => true, ]); - + $activeAccount2 = PlatformAccount::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'is_active' => true + 'is_active' => true, ]); - + $inactiveAccount = PlatformAccount::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'is_active' => false + 'is_active' => false, ]); $activeAccounts = PlatformAccount::getActive(PlatformEnum::LEMMY); @@ -146,17 +144,17 @@ public function test_set_as_active_method(): void // Create multiple accounts for same platform $account1 = PlatformAccount::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'is_active' => true + 'is_active' => true, ]); - + $account2 = PlatformAccount::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'is_active' => true + 'is_active' => true, ]); - + $account3 = PlatformAccount::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'is_active' => false + 'is_active' => false, ]); // Set account3 as active @@ -182,12 +180,12 @@ public function test_belongs_to_many_channels_relationship(): void // Attach channels with pivot data $account->channels()->attach($channel1->id, [ 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); $account->channels()->attach($channel2->id, [ 'is_active' => false, - 'priority' => 50 + 'priority' => 50, ]); $channels = $account->channels; @@ -216,17 +214,17 @@ public function test_active_channels_relationship(): void // Attach channels $account->channels()->attach($activeChannel1->id, [ 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); $account->channels()->attach($activeChannel2->id, [ 'is_active' => true, - 'priority' => 200 + 'priority' => 200, ]); $account->channels()->attach($inactiveChannel->id, [ 'is_active' => false, - 'priority' => 150 + 'priority' => 150, ]); $activeChannels = $account->activeChannels; @@ -271,7 +269,7 @@ public function test_account_creation_with_explicit_values(): void 'settings' => $settings, 'is_active' => false, 'last_tested_at' => $timestamp, - 'status' => 'working' + 'status' => 'working', ]); $this->assertEquals(PlatformEnum::LEMMY, $account->platform); @@ -302,12 +300,12 @@ public function test_account_update(): void { $account = PlatformAccount::factory()->create([ 'username' => 'original_user', - 'is_active' => true + 'is_active' => true, ]); $account->update([ 'username' => 'updated_user', - 'is_active' => false + 'is_active' => false, ]); $account->refresh(); @@ -339,13 +337,13 @@ public function test_account_settings_can_be_complex_structure(): void $complexSettings = [ 'authentication' => [ 'method' => 'jwt', - 'timeout' => 30 + 'timeout' => 30, ], 'features' => ['posting', 'commenting'], 'rate_limits' => [ 'posts_per_hour' => 10, - 'comments_per_hour' => 50 - ] + 'comments_per_hour' => 50, + ], ]; $account = PlatformAccount::factory()->create(['settings' => $complexSettings]); @@ -383,7 +381,7 @@ public function test_account_can_have_multiple_channels_with_different_prioritie $account->channels()->attach([ $channel1->id => ['is_active' => true, 'priority' => 300], $channel2->id => ['is_active' => true, 'priority' => 100], - $channel3->id => ['is_active' => false, 'priority' => 200] + $channel3->id => ['is_active' => false, 'priority' => 200], ]); $allChannels = $account->channels; @@ -399,19 +397,19 @@ public function test_account_can_have_multiple_channels_with_different_prioritie } } - public function test_password_withoutObjectCaching_prevents_caching(): void + public function test_password_without_object_caching_prevents_caching(): void { $account = PlatformAccount::factory()->create(['password' => 'original']); - + // Access password to potentially cache it $originalPassword = $account->password; $this->assertEquals('original', $originalPassword); - + // Update password directly in database $account->update(['password' => 'updated']); - + // Since withoutObjectCaching is used, the new value should be retrieved $account->refresh(); $this->assertEquals('updated', $account->password); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/PlatformChannelTest.php b/tests/Unit/Models/PlatformChannelTest.php index 39e5e5c..9d08a2e 100644 --- a/tests/Unit/Models/PlatformChannelTest.php +++ b/tests/Unit/Models/PlatformChannelTest.php @@ -18,15 +18,15 @@ class PlatformChannelTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['platform_instance_id', 'name', 'display_name', 'channel_id', 'description', 'language_id', 'is_active']; - $channel = new PlatformChannel(); - + $channel = new PlatformChannel; + $this->assertEquals($fillableFields, $channel->getFillable()); } public function test_table_name(): void { - $channel = new PlatformChannel(); - + $channel = new PlatformChannel; + $this->assertEquals('platform_channels', $channel->getTable()); } @@ -39,7 +39,7 @@ public function test_casts_is_active_to_boolean(): void $channel->update(['is_active' => '0']); $channel->refresh(); - + $this->assertIsBool($channel->is_active); $this->assertFalse($channel->is_active); } @@ -73,12 +73,12 @@ public function test_belongs_to_many_platform_accounts_relationship(): void // Attach accounts with pivot data $channel->platformAccounts()->attach($account1->id, [ 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); $channel->platformAccounts()->attach($account2->id, [ 'is_active' => false, - 'priority' => 50 + 'priority' => 50, ]); $accounts = $channel->platformAccounts; @@ -107,17 +107,17 @@ public function test_active_platform_accounts_relationship(): void // Attach accounts $channel->platformAccounts()->attach($activeAccount1->id, [ 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); $channel->platformAccounts()->attach($activeAccount2->id, [ 'is_active' => true, - 'priority' => 200 + 'priority' => 200, ]); $channel->platformAccounts()->attach($inactiveAccount->id, [ 'is_active' => false, - 'priority' => 150 + 'priority' => 150, ]); $activeAccounts = $channel->activePlatformAccounts; @@ -133,7 +133,7 @@ public function test_full_name_attribute(): void $instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.example.com']); $channel = PlatformChannel::factory()->create([ 'platform_instance_id' => $instance->id, - 'name' => 'technology' + 'name' => 'technology', ]); $this->assertEquals('https://lemmy.example.com/c/technology', $channel->full_name); @@ -150,14 +150,14 @@ public function test_belongs_to_many_feeds_relationship(): void 'feed_id' => $feed1->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); Route::create([ 'feed_id' => $feed2->id, 'platform_channel_id' => $channel->id, 'is_active' => false, - 'priority' => 50 + 'priority' => 50, ]); $feeds = $channel->feeds; @@ -184,21 +184,21 @@ public function test_active_feeds_relationship(): void 'feed_id' => $activeFeed1->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); Route::create([ 'feed_id' => $activeFeed2->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 200 + 'priority' => 200, ]); Route::create([ 'feed_id' => $inactiveFeed->id, 'platform_channel_id' => $channel->id, 'is_active' => false, - 'priority' => 150 + 'priority' => 150, ]); $activeFeeds = $channel->activeFeeds; @@ -237,7 +237,7 @@ public function test_channel_creation_with_explicit_values(): void 'channel_id' => 'channel_123', 'description' => 'A test channel', 'language_id' => $language->id, - 'is_active' => false + 'is_active' => false, ]); $this->assertEquals($instance->id, $channel->platform_instance_id); @@ -253,12 +253,12 @@ public function test_channel_update(): void { $channel = PlatformChannel::factory()->create([ 'name' => 'original_name', - 'is_active' => true + 'is_active' => true, ]); $channel->update([ 'name' => 'updated_name', - 'is_active' => false + 'is_active' => false, ]); $channel->refresh(); @@ -281,7 +281,7 @@ public function test_channel_with_display_name(): void { $channel = PlatformChannel::factory()->create([ 'name' => 'tech', - 'display_name' => 'Technology Discussion' + 'display_name' => 'Technology Discussion', ]); $this->assertEquals('tech', $channel->name); @@ -292,7 +292,7 @@ public function test_channel_without_display_name(): void { $channel = PlatformChannel::factory()->create([ 'name' => 'general', - 'display_name' => 'General' + 'display_name' => 'General', ]); $this->assertEquals('general', $channel->name); @@ -320,7 +320,7 @@ public function test_channel_can_have_multiple_accounts_with_different_prioritie $channel->platformAccounts()->attach([ $account1->id => ['is_active' => true, 'priority' => 300], $account2->id => ['is_active' => true, 'priority' => 100], - $account3->id => ['is_active' => false, 'priority' => 200] + $account3->id => ['is_active' => false, 'priority' => 200], ]); $allAccounts = $channel->platformAccounts; @@ -335,4 +335,4 @@ public function test_channel_can_have_multiple_accounts_with_different_prioritie $this->assertIsInt($account->pivot->is_active); } } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/PlatformInstanceTest.php b/tests/Unit/Models/PlatformInstanceTest.php index 9463493..296515f 100644 --- a/tests/Unit/Models/PlatformInstanceTest.php +++ b/tests/Unit/Models/PlatformInstanceTest.php @@ -16,15 +16,15 @@ class PlatformInstanceTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['platform', 'url', 'name', 'description', 'is_active']; - $instance = new PlatformInstance(); - + $instance = new PlatformInstance; + $this->assertEquals($fillableFields, $instance->getFillable()); } public function test_table_name(): void { - $instance = new PlatformInstance(); - + $instance = new PlatformInstance; + $this->assertEquals('platform_instances', $instance->getTable()); } @@ -46,7 +46,7 @@ public function test_casts_is_active_to_boolean(): void $instance->update(['is_active' => '0']); $instance->refresh(); - + $this->assertIsBool($instance->is_active); $this->assertFalse($instance->is_active); } @@ -54,10 +54,10 @@ public function test_casts_is_active_to_boolean(): void public function test_has_many_channels_relationship(): void { $instance = PlatformInstance::factory()->create(); - + $channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]); $channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $instance->id]); - + // Create channel for different instance $otherInstance = PlatformInstance::factory()->create(); PlatformChannel::factory()->create(['platform_instance_id' => $otherInstance->id]); @@ -79,12 +79,12 @@ public function test_belongs_to_many_languages_relationship(): void // Attach languages with pivot data $instance->languages()->attach($language1->id, [ 'platform_language_id' => 1, - 'is_default' => true + 'is_default' => true, ]); $instance->languages()->attach($language2->id, [ 'platform_language_id' => 2, - 'is_default' => false + 'is_default' => false, ]); $languages = $instance->languages; @@ -96,26 +96,26 @@ public function test_belongs_to_many_languages_relationship(): void // Test pivot data $language1FromRelation = $languages->find($language1->id); $this->assertEquals(1, $language1FromRelation->pivot->platform_language_id); - $this->assertEquals(1, $language1FromRelation->pivot->is_default); // Database returns 1 for true + $this->assertEquals(1, $language1FromRelation->pivot->is_default); $language2FromRelation = $languages->find($language2->id); $this->assertEquals(2, $language2FromRelation->pivot->platform_language_id); - $this->assertEquals(0, $language2FromRelation->pivot->is_default); // Database returns 0 for false + $this->assertEquals(0, $language2FromRelation->pivot->is_default); } public function test_find_by_url_static_method(): void { $url = 'https://lemmy.world'; - + $instance1 = PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'url' => $url + 'url' => $url, ]); - + // Create instance with different URL PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'url' => 'https://lemmy.ml' + 'url' => 'https://lemmy.ml', ]); $foundInstance = PlatformInstance::findByUrl(PlatformEnum::LEMMY, $url); @@ -136,11 +136,11 @@ public function test_find_by_url_returns_null_when_not_found(): void public function test_find_by_url_filters_by_platform(): void { $url = 'https://example.com'; - + // Create instance with same URL but different platform won't be found PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'url' => $url + 'url' => $url, ]); // Since we only have LEMMY in the enum, this test demonstrates the filtering logic @@ -166,7 +166,7 @@ public function test_instance_creation_with_explicit_values(): void 'url' => 'https://lemmy.world', 'name' => 'Lemmy World', 'description' => 'A general purpose Lemmy instance', - 'is_active' => false + 'is_active' => false, ]); $this->assertEquals(PlatformEnum::LEMMY, $instance->platform); @@ -191,12 +191,12 @@ public function test_instance_update(): void { $instance = PlatformInstance::factory()->create([ 'name' => 'Original Name', - 'is_active' => true + 'is_active' => true, ]); $instance->update([ 'name' => 'Updated Name', - 'is_active' => false + 'is_active' => false, ]); $instance->refresh(); @@ -265,7 +265,7 @@ public function test_instance_can_have_multiple_languages(): void $instance->languages()->attach([ $language1->id => ['platform_language_id' => 1, 'is_default' => true], $language2->id => ['platform_language_id' => 2, 'is_default' => false], - $language3->id => ['platform_language_id' => 3, 'is_default' => false] + $language3->id => ['platform_language_id' => 3, 'is_default' => false], ]); $languages = $instance->languages; @@ -275,11 +275,11 @@ public function test_instance_can_have_multiple_languages(): void // Test that we can access pivot data foreach ($languages as $language) { $this->assertNotNull($language->pivot->platform_language_id); - $this->assertContains($language->pivot->is_default, [0, 1, true, false]); // Can be int or bool + $this->assertContains($language->pivot->is_default, [0, 1, true, false]); } // Only one should be default - $defaultLanguages = $languages->filter(fn($lang) => $lang->pivot->is_default); + $defaultLanguages = $languages->filter(fn ($lang) => $lang->pivot->is_default); $this->assertCount(1, $defaultLanguages); } @@ -301,12 +301,12 @@ public function test_multiple_instances_with_same_platform(): void { $instance1 = PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'name' => 'Lemmy World' + 'name' => 'Lemmy World', ]); $instance2 = PlatformInstance::factory()->create([ 'platform' => PlatformEnum::LEMMY, - 'name' => 'Lemmy ML' + 'name' => 'Lemmy ML', ]); $this->assertEquals(PlatformEnum::LEMMY, $instance1->platform); @@ -322,4 +322,4 @@ public function test_instance_platform_enum_string_value(): void $this->assertEquals('lemmy', $instance->platform->value); $this->assertEquals(PlatformEnum::LEMMY, $instance->platform); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/RouteTest.php b/tests/Unit/Models/RouteTest.php index af5ff16..1903e43 100644 --- a/tests/Unit/Models/RouteTest.php +++ b/tests/Unit/Models/RouteTest.php @@ -16,8 +16,8 @@ class RouteTest extends TestCase public function test_fillable_fields(): void { $fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority']; - $route = new Route(); - + $route = new Route; + $this->assertEquals($fillableFields, $route->getFillable()); } @@ -25,12 +25,12 @@ public function test_casts_is_active_to_boolean(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $route = Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => '1', - 'priority' => 50 + 'priority' => 50, ]); $this->assertIsBool($route->is_active); @@ -38,23 +38,23 @@ public function test_casts_is_active_to_boolean(): void $route->update(['is_active' => '0']); $route->refresh(); - + $this->assertIsBool($route->is_active); $this->assertFalse($route->is_active); } public function test_primary_key_configuration(): void { - $route = new Route(); - + $route = new Route; + $this->assertNull($route->getKeyName()); $this->assertFalse($route->getIncrementing()); } public function test_table_name(): void { - $route = new Route(); - + $route = new Route; + $this->assertEquals('routes', $route->getTable()); } @@ -62,12 +62,12 @@ public function test_belongs_to_feed_relationship(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $route = Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $this->assertInstanceOf(Feed::class, $route->feed); @@ -79,12 +79,12 @@ public function test_belongs_to_platform_channel_relationship(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $route = Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $this->assertInstanceOf(PlatformChannel::class, $route->platformChannel); @@ -96,25 +96,27 @@ public function test_has_many_keywords_relationship(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $route = Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Create keywords for this route + /** @var Keyword $keyword1 */ $keyword1 = Keyword::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'keyword' => 'test1' + 'keyword' => 'test1', ]); + /** @var Keyword $keyword2 */ $keyword2 = Keyword::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, - 'keyword' => 'test2' + 'keyword' => 'test2', ]); // Create keyword for different route (should not be included) @@ -122,7 +124,7 @@ public function test_has_many_keywords_relationship(): void Keyword::factory()->create([ 'feed_id' => $otherFeed->id, 'platform_channel_id' => $channel->id, - 'keyword' => 'other' + 'keyword' => 'other', ]); $keywords = $route->keywords; @@ -139,33 +141,34 @@ public function test_keywords_relationship_filters_by_feed_and_channel(): void $feed2 = Feed::factory()->create(); $channel1 = PlatformChannel::factory()->create(); $channel2 = PlatformChannel::factory()->create(); - + $route = Route::create([ 'feed_id' => $feed1->id, 'platform_channel_id' => $channel1->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Create keyword for this exact route + /** @var Keyword $matchingKeyword */ $matchingKeyword = Keyword::factory()->create([ 'feed_id' => $feed1->id, 'platform_channel_id' => $channel1->id, - 'keyword' => 'matching' + 'keyword' => 'matching', ]); // Create keyword for same feed but different channel Keyword::factory()->create([ 'feed_id' => $feed1->id, 'platform_channel_id' => $channel2->id, - 'keyword' => 'different_channel' + 'keyword' => 'different_channel', ]); // Create keyword for same channel but different feed Keyword::factory()->create([ 'feed_id' => $feed2->id, 'platform_channel_id' => $channel1->id, - 'keyword' => 'different_feed' + 'keyword' => 'different_feed', ]); $keywords = $route->keywords; @@ -195,7 +198,7 @@ public function test_route_creation_with_explicit_values(): void 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => false, - 'priority' => 75 + 'priority' => 75, ]); $this->assertEquals($feed->id, $route->feed_id); @@ -206,14 +209,15 @@ public function test_route_creation_with_explicit_values(): void public function test_route_update(): void { + /** @var Route $route */ $route = Route::factory()->create([ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $route->update([ 'is_active' => false, - 'priority' => 25 + 'priority' => 25, ]); $route->refresh(); @@ -226,26 +230,26 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); - + $route = Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); Keyword::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'active_keyword', - 'is_active' => true + 'is_active' => true, ]); Keyword::factory()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'keyword' => 'inactive_keyword', - 'is_active' => false + 'is_active' => false, ]); $keywords = $route->keywords; @@ -258,4 +262,4 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void $this->assertEquals('active_keyword', $activeKeywords->first()->keyword); $this->assertEquals('inactive_keyword', $inactiveKeywords->first()->keyword); } -} \ No newline at end of file +} diff --git a/tests/Unit/Modules/Lemmy/LemmyRequestTest.php b/tests/Unit/Modules/Lemmy/LemmyRequestTest.php index 18cc955..b8404a2 100644 --- a/tests/Unit/Modules/Lemmy/LemmyRequestTest.php +++ b/tests/Unit/Modules/Lemmy/LemmyRequestTest.php @@ -12,7 +12,7 @@ class LemmyRequestTest extends TestCase public function test_constructor_with_simple_domain(): void { $request = new LemmyRequest('lemmy.world'); - + $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); $this->assertNull($this->getPrivateProperty($request, 'token')); @@ -21,7 +21,7 @@ public function test_constructor_with_simple_domain(): void public function test_constructor_with_https_url(): void { $request = new LemmyRequest('https://lemmy.world'); - + $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); } @@ -29,7 +29,7 @@ public function test_constructor_with_https_url(): void public function test_constructor_with_http_url(): void { $request = new LemmyRequest('http://lemmy.world'); - + $this->assertEquals('http', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); } @@ -37,14 +37,14 @@ public function test_constructor_with_http_url(): void public function test_constructor_with_trailing_slash(): void { $request = new LemmyRequest('lemmy.world/'); - + $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); } public function test_constructor_with_full_url_and_trailing_slash(): void { $request = new LemmyRequest('https://lemmy.world/'); - + $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); $this->assertEquals('lemmy.world', $this->getPrivateProperty($request, 'instance')); } @@ -52,14 +52,14 @@ public function test_constructor_with_full_url_and_trailing_slash(): void public function test_constructor_with_token(): void { $request = new LemmyRequest('lemmy.world', 'test-token'); - + $this->assertEquals('test-token', $this->getPrivateProperty($request, 'token')); } public function test_constructor_preserves_case_in_scheme_detection(): void { $request = new LemmyRequest('HTTPS://lemmy.world'); - + $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); } @@ -67,7 +67,7 @@ public function test_with_scheme_sets_https(): void { $request = new LemmyRequest('lemmy.world'); $result = $request->withScheme('https'); - + $this->assertSame($request, $result); $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); } @@ -76,7 +76,7 @@ public function test_with_scheme_sets_http(): void { $request = new LemmyRequest('lemmy.world'); $result = $request->withScheme('http'); - + $this->assertSame($request, $result); $this->assertEquals('http', $this->getPrivateProperty($request, 'scheme')); } @@ -85,7 +85,7 @@ public function test_with_scheme_normalizes_case(): void { $request = new LemmyRequest('lemmy.world'); $request->withScheme('HTTPS'); - + $this->assertEquals('https', $this->getPrivateProperty($request, 'scheme')); } @@ -93,9 +93,9 @@ public function test_with_scheme_ignores_invalid_schemes(): void { $request = new LemmyRequest('lemmy.world'); $originalScheme = $this->getPrivateProperty($request, 'scheme'); - + $request->withScheme('ftp'); - + $this->assertEquals($originalScheme, $this->getPrivateProperty($request, 'scheme')); } @@ -103,7 +103,7 @@ public function test_with_token_sets_token(): void { $request = new LemmyRequest('lemmy.world'); $result = $request->withToken('new-token'); - + $this->assertSame($request, $result); $this->assertEquals('new-token', $this->getPrivateProperty($request, 'token')); } @@ -111,27 +111,27 @@ public function test_with_token_sets_token(): void public function test_get_without_token(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $response = $request->get('site'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'https://lemmy.world/api/v3/site' - && !$httpRequest->hasHeader('Authorization'); + && ! $httpRequest->hasHeader('Authorization'); }); } public function test_get_with_token(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world', 'test-token'); $response = $request->get('site'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'https://lemmy.world/api/v3/site' && $httpRequest->header('Authorization')[0] === 'Bearer test-token'; @@ -141,15 +141,16 @@ public function test_get_with_token(): void public function test_get_with_parameters(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $params = ['limit' => 10, 'page' => 1]; $response = $request->get('posts', $params); - + $this->assertInstanceOf(Response::class, $response); - - Http::assertSent(function ($httpRequest) use ($params) { + + Http::assertSent(function ($httpRequest) { $url = $httpRequest->url(); + return str_contains($url, 'https://lemmy.world/api/v3/posts') && str_contains($url, 'limit=10') && str_contains($url, 'page=1'); @@ -159,13 +160,13 @@ public function test_get_with_parameters(): void public function test_get_with_http_scheme(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $request->withScheme('http'); $response = $request->get('site'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'http://lemmy.world/api/v3/site'; }); @@ -174,28 +175,28 @@ public function test_get_with_http_scheme(): void public function test_post_without_token(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $response = $request->post('login'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'https://lemmy.world/api/v3/login' && $httpRequest->method() === 'POST' - && !$httpRequest->hasHeader('Authorization'); + && ! $httpRequest->hasHeader('Authorization'); }); } public function test_post_with_token(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world', 'test-token'); $response = $request->post('login'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'https://lemmy.world/api/v3/login' && $httpRequest->method() === 'POST' @@ -206,13 +207,13 @@ public function test_post_with_token(): void public function test_post_with_data(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $data = ['username' => 'test', 'password' => 'pass']; $response = $request->post('login', $data); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) use ($data) { return $httpRequest->url() === 'https://lemmy.world/api/v3/login' && $httpRequest->method() === 'POST' @@ -223,13 +224,13 @@ public function test_post_with_data(): void public function test_post_with_http_scheme(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $request->withScheme('http'); $response = $request->post('login'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'http://lemmy.world/api/v3/login'; }); @@ -238,10 +239,10 @@ public function test_post_with_http_scheme(): void public function test_requests_use_30_second_timeout(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $request->get('site'); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'https://lemmy.world/api/v3/site'; }); @@ -250,12 +251,12 @@ public function test_requests_use_30_second_timeout(): void public function test_chaining_methods(): void { Http::fake(['*' => Http::response(['success' => true])]); - + $request = new LemmyRequest('lemmy.world'); $response = $request->withScheme('http')->withToken('chained-token')->get('site'); - + $this->assertInstanceOf(Response::class, $response); - + Http::assertSent(function ($httpRequest) { return $httpRequest->url() === 'http://lemmy.world/api/v3/site' && $httpRequest->header('Authorization')[0] === 'Bearer chained-token'; @@ -267,7 +268,7 @@ private function getPrivateProperty(object $object, string $property): mixed $reflection = new \ReflectionClass($object); $reflectionProperty = $reflection->getProperty($property); $reflectionProperty->setAccessible(true); - + return $reflectionProperty->getValue($object); } -} \ No newline at end of file +} diff --git a/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php b/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php index 75bbb72..2d8caf8 100644 --- a/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php +++ b/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php @@ -2,12 +2,12 @@ namespace Tests\Unit\Modules\Lemmy\Services; -use App\Modules\Lemmy\Services\LemmyApiService; use App\Enums\PlatformEnum; +use App\Modules\Lemmy\Services\LemmyApiService; +use Exception; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Tests\TestCase; -use Exception; class LemmyApiServiceTest extends TestCase { @@ -27,7 +27,7 @@ public function test_constructor_sets_instance(): void public function test_login_with_https_success(): void { 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'); @@ -46,7 +46,7 @@ public function test_login_falls_back_to_http_on_https_failure(): void { Http::fake([ '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'); @@ -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 { 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'); @@ -76,7 +76,7 @@ public function test_login_with_explicit_http_scheme(): void public function test_login_with_explicit_https_scheme(): void { 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'); @@ -92,7 +92,7 @@ public function test_login_with_explicit_https_scheme(): void public function test_login_returns_null_on_unsuccessful_response(): void { Http::fake([ - '*' => Http::response(['error' => 'Invalid credentials'], 401) + '*' => Http::response(['error' => 'Invalid credentials'], 401), ]); $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 { Http::fake([ - '*' => Http::response('{"error":"rate_limit_error"}', 429) + '*' => Http::response('{"error":"rate_limit_error"}', 429), ]); $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 { Http::fake([ - '*' => Http::response(['success' => true], 200) + '*' => Http::response(['success' => true], 200), ]); $service = new LemmyApiService('lemmy.world'); @@ -146,9 +146,9 @@ public function test_get_community_id_success(): void Http::fake([ '*' => Http::response([ 'community_view' => [ - 'community' => ['id' => 123] - ] - ], 200) + 'community' => ['id' => 123], + ], + ], 200), ]); $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 { Http::fake([ - '*' => Http::response('Not found', 404) + '*' => Http::response('Not found', 404), ]); $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 { Http::fake([ - '*' => Http::response(['success' => true], 200) + '*' => Http::response(['success' => true], 200), ]); $service = new LemmyApiService('lemmy.world'); @@ -201,19 +201,19 @@ public function test_sync_channel_posts_success(): void 'id' => 1, 'url' => 'https://example.com/1', 'name' => 'Post 1', - 'published' => '2024-01-01T00:00:00Z' - ] + 'published' => '2024-01-01T00:00:00Z', + ], ], [ 'post' => [ 'id' => 2, 'url' => 'https://example.com/2', 'name' => 'Post 2', - 'published' => '2024-01-02T00:00:00Z' - ] - ] - ] - ], 200) + 'published' => '2024-01-02T00:00:00Z', + ], + ], + ], + ], 200), ]); $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 { Http::fake([ - '*' => Http::response('Error', 500) + '*' => Http::response('Error', 500), ]); $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 { Http::fake([ - '*' => Http::response(['post_view' => ['post' => ['id' => 999]]], 200) + '*' => Http::response(['post_view' => ['post' => ['id' => 999]]], 200), ]); $service = new LemmyApiService('lemmy.world'); @@ -293,6 +293,7 @@ public function test_create_post_with_all_parameters(): void Http::assertSent(function ($request) { $data = $request->data(); + return $request->url() === 'https://lemmy.world/api/v3/post' && $data['name'] === 'Test Title' && $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 { Http::fake([ - '*' => Http::response(['post_view' => ['post' => ['id' => 888]]], 200) + '*' => Http::response(['post_view' => ['post' => ['id' => 888]]], 200), ]); $service = new LemmyApiService('lemmy.world'); @@ -321,20 +322,21 @@ public function test_create_post_with_minimal_parameters(): void Http::assertSent(function ($request) { $data = $request->data(); + return $request->url() === 'https://lemmy.world/api/v3/post' && $data['name'] === 'Title Only' && $data['body'] === 'Body Only' && $data['community_id'] === 42 - && !isset($data['url']) - && !isset($data['custom_thumbnail']) - && !isset($data['language_id']); + && ! isset($data['url']) + && ! isset($data['custom_thumbnail']) + && ! isset($data['language_id']); }); } public function test_create_post_throws_on_unsuccessful_response(): void { Http::fake([ - '*' => Http::response('Forbidden', 403) + '*' => Http::response('Forbidden', 403), ]); $service = new LemmyApiService('lemmy.world'); @@ -351,9 +353,9 @@ public function test_get_languages_success(): void '*' => Http::response([ 'all_languages' => [ ['id' => 1, 'code' => 'en', 'name' => 'English'], - ['id' => 2, 'code' => 'fr', 'name' => 'French'] - ] - ], 200) + ['id' => 2, 'code' => 'fr', 'name' => 'French'], + ], + ], 200), ]); $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 { Http::fake([ - '*' => Http::response('Error', 500) + '*' => Http::response('Error', 500), ]); $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 { Http::fake([ - '*' => Http::response(['site_view' => []], 200) + '*' => Http::response(['site_view' => []], 200), ]); $service = new LemmyApiService('lemmy.world'); diff --git a/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php b/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php index 5f17c6b..6e53676 100644 --- a/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php +++ b/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php @@ -2,22 +2,23 @@ namespace Tests\Unit\Modules\Lemmy\Services; -use App\Modules\Lemmy\Services\LemmyPublisher; -use App\Modules\Lemmy\Services\LemmyApiService; -use App\Services\Auth\LemmyAuthService; +use App\Enums\PlatformEnum; +use App\Exceptions\PlatformAuthException; use App\Models\Article; use App\Models\PlatformAccount; use App\Models\PlatformChannel; -use App\Exceptions\PlatformAuthException; -use App\Enums\PlatformEnum; -use Illuminate\Foundation\Testing\RefreshDatabase; -use Tests\TestCase; -use Mockery; +use App\Modules\Lemmy\Services\LemmyApiService; +use App\Modules\Lemmy\Services\LemmyPublisher; +use App\Services\Auth\LemmyAuthService; use Exception; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Mockery; +use Tests\TestCase; class LemmyPublisherTest extends TestCase { use RefreshDatabase; + protected function tearDown(): void { Mockery::close(); @@ -27,17 +28,17 @@ protected function tearDown(): void public function test_constructor_initializes_api_service(): void { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $publisher = new LemmyPublisher($account); $reflection = new \ReflectionClass($publisher); - + $apiProperty = $reflection->getProperty('api'); $apiProperty->setAccessible(true); $this->assertInstanceOf(LemmyApiService::class, $apiProperty->getValue($publisher)); - + $accountProperty = $reflection->getProperty('account'); $accountProperty->setAccessible(true); $this->assertSame($account, $accountProperty->getValue($publisher)); @@ -46,22 +47,22 @@ public function test_constructor_initializes_api_service(): void public function test_publish_to_channel_with_all_data(): void { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $article = Article::factory()->make([ - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $channel = PlatformChannel::factory()->make([ - 'channel_id' => '42' + 'channel_id' => '42', ]); $extractedData = [ 'title' => 'Test Article', 'description' => 'Test Description', 'thumbnail' => 'https://example.com/thumb.jpg', - 'language_id' => 5 + 'language_id' => 5, ]; // Mock LemmyAuthService via service container @@ -70,7 +71,7 @@ public function test_publish_to_channel_with_all_data(): void ->once() ->with($account) ->andReturn('test-token'); - + $this->app->instance(LemmyAuthService::class, $authMock); // Mock LemmyApiService @@ -90,7 +91,7 @@ public function test_publish_to_channel_with_all_data(): void // Create publisher and inject mocked API using reflection $publisher = new LemmyPublisher($account); - + $reflection = new \ReflectionClass($publisher); $apiProperty = $reflection->getProperty('api'); $apiProperty->setAccessible(true); @@ -104,15 +105,15 @@ public function test_publish_to_channel_with_all_data(): void public function test_publish_to_channel_with_minimal_data(): void { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $article = Article::factory()->make([ - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $channel = PlatformChannel::factory()->make([ - 'channel_id' => '24' + 'channel_id' => '24', ]); $extractedData = []; @@ -123,7 +124,7 @@ public function test_publish_to_channel_with_minimal_data(): void ->once() ->with($account) ->andReturn('minimal-token'); - + $this->app->instance(LemmyAuthService::class, $authMock); // Mock LemmyApiService @@ -143,7 +144,7 @@ public function test_publish_to_channel_with_minimal_data(): void // Create publisher and inject mocked API using reflection $publisher = new LemmyPublisher($account); - + $reflection = new \ReflectionClass($publisher); $apiProperty = $reflection->getProperty('api'); $apiProperty->setAccessible(true); @@ -157,21 +158,21 @@ public function test_publish_to_channel_with_minimal_data(): void public function test_publish_to_channel_without_thumbnail(): void { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $article = Article::factory()->make([ - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $channel = PlatformChannel::factory()->make([ - 'channel_id' => '33' + 'channel_id' => '33', ]); $extractedData = [ 'title' => 'No Thumbnail Article', 'description' => 'Article without thumbnail', - 'language_id' => 2 + 'language_id' => 2, ]; // Mock LemmyAuthService @@ -199,7 +200,7 @@ public function test_publish_to_channel_without_thumbnail(): void // Create publisher and inject mocked API using reflection $publisher = new LemmyPublisher($account); - + $reflection = new \ReflectionClass($publisher); $apiProperty = $reflection->getProperty('api'); $apiProperty->setAccessible(true); @@ -213,7 +214,7 @@ public function test_publish_to_channel_without_thumbnail(): void public function test_publish_to_channel_throws_platform_auth_exception(): void { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $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 { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $article = Article::factory()->make([ - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $channel = PlatformChannel::factory()->make([ - 'channel_id' => '42' + 'channel_id' => '42', ]); $extractedData = [ - 'title' => 'Test Article' + 'title' => 'Test Article', ]; // Mock LemmyAuthService via service container @@ -260,7 +261,7 @@ public function test_publish_to_channel_throws_api_exception(): void ->once() ->with($account) ->andReturn('test-token'); - + $this->app->instance(LemmyAuthService::class, $authMock); // Mock LemmyApiService to throw exception @@ -271,7 +272,7 @@ public function test_publish_to_channel_throws_api_exception(): void // Create publisher and inject mocked API using reflection $publisher = new LemmyPublisher($account); - + $reflection = new \ReflectionClass($publisher); $apiProperty = $reflection->getProperty('api'); $apiProperty->setAccessible(true); @@ -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 { $account = PlatformAccount::factory()->make([ - 'instance_url' => 'https://lemmy.world' + 'instance_url' => 'https://lemmy.world', ]); $article = Article::factory()->make([ - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $channel = PlatformChannel::factory()->make([ - 'channel_id' => 'string-42' + 'channel_id' => 'string-42', ]); $extractedData = [ - 'title' => 'Test Title' + 'title' => 'Test Title', ]; // Mock LemmyAuthService @@ -329,7 +330,7 @@ public function test_publish_to_channel_handles_string_channel_id(): void // Create publisher and inject mocked API using reflection $publisher = new LemmyPublisher($account); - + $reflection = new \ReflectionClass($publisher); $apiProperty = $reflection->getProperty('api'); $apiProperty->setAccessible(true); @@ -339,4 +340,4 @@ public function test_publish_to_channel_handles_string_channel_id(): void $this->assertEquals(['success' => true], $result); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/ArticleFetcherRssTest.php b/tests/Unit/Services/ArticleFetcherRssTest.php index 0479d6a..425ae23 100644 --- a/tests/Unit/Services/ArticleFetcherRssTest.php +++ b/tests/Unit/Services/ArticleFetcherRssTest.php @@ -12,7 +12,7 @@ class ArticleFetcherRssTest extends TestCase { - use RefreshDatabase, CreatesArticleFetcher; + use CreatesArticleFetcher, RefreshDatabase; private string $sampleRss; @@ -156,9 +156,55 @@ public function test_get_articles_from_rss_feed_handles_http_failure(): void $this->assertEmpty($result); } + public function test_get_articles_from_belga_rss_feed_creates_articles(): void + { + $belgaRss = <<<'XML' +<?xml version="1.0" encoding="UTF-8"?> +<rss version="2.0"> + <channel> + <title>Belga News Agency + https://www.belganewsagency.eu + + Belgium announces new climate plan + https://www.belganewsagency.eu/belgium-announces-new-climate-plan + Belgium has unveiled a comprehensive climate strategy. + Sun, 08 Mar 2026 10:00:00 GMT + + + EU summit concludes in Brussels + https://www.belganewsagency.eu/eu-summit-concludes-in-brussels + European leaders reached agreement on key issues. + Sun, 08 Mar 2026 09:00:00 GMT + + + +XML; + + Http::fake(['*' => Http::response($belgaRss, 200)]); + + $feed = Feed::factory()->create([ + 'type' => 'rss', + 'provider' => 'belga', + 'url' => 'https://www.belganewsagency.eu/feed', + ]); + + $fetcher = $this->createArticleFetcher(); + $result = $fetcher->getArticlesFromFeed($feed); + + $this->assertCount(2, $result); + $this->assertDatabaseHas('articles', [ + 'url' => 'https://www.belganewsagency.eu/belgium-announces-new-climate-plan', + 'feed_id' => $feed->id, + ]); + $this->assertDatabaseHas('articles', [ + 'url' => 'https://www.belganewsagency.eu/eu-summit-concludes-in-brussels', + 'feed_id' => $feed->id, + ]); + } + protected function tearDown(): void { Mockery::close(); parent::tearDown(); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/ArticleFetcherTest.php b/tests/Unit/Services/ArticleFetcherTest.php index c67c680..180b099 100644 --- a/tests/Unit/Services/ArticleFetcherTest.php +++ b/tests/Unit/Services/ArticleFetcherTest.php @@ -2,19 +2,18 @@ namespace Tests\Unit\Services; -use App\Services\Article\ArticleFetcher; -use App\Services\Log\LogSaver; -use App\Models\Feed; use App\Models\Article; -use Tests\TestCase; -use Tests\Traits\CreatesArticleFetcher; +use App\Models\Feed; +use App\Services\Article\ArticleFetcher; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Mockery; +use Tests\TestCase; +use Tests\Traits\CreatesArticleFetcher; class ArticleFetcherTest extends TestCase { - use RefreshDatabase, CreatesArticleFetcher; + use CreatesArticleFetcher, RefreshDatabase; protected function setUp(): void { @@ -22,7 +21,7 @@ protected function setUp(): void // Mock all HTTP requests by default to prevent external calls Http::fake([ - '*' => Http::response('Mock HTML content', 200) + '*' => Http::response('Mock HTML content', 200), ]); // Create ArticleFetcher only when needed - tests will create their own @@ -31,10 +30,10 @@ protected function setUp(): void public function test_get_articles_from_feed_returns_collection(): void { $articleFetcher = $this->createArticleFetcher(); - + $feed = Feed::factory()->create([ 'type' => 'rss', - 'url' => 'https://example.com/feed.rss' + 'url' => 'https://example.com/feed.rss', ]); $result = $articleFetcher->getArticlesFromFeed($feed); @@ -46,7 +45,7 @@ public function test_get_articles_from_rss_feed_returns_empty_collection(): void { $feed = Feed::factory()->create([ 'type' => 'rss', - 'url' => 'https://example.com/feed.rss' + 'url' => 'https://example.com/feed.rss', ]); $articleFetcher = $this->createArticleFetcher(); @@ -60,7 +59,7 @@ public function test_get_articles_from_website_feed_handles_no_parser(): void { $feed = Feed::factory()->create([ 'type' => 'website', - 'url' => 'https://unsupported-site.com/' + 'url' => 'https://unsupported-site.com/', ]); $articleFetcher = $this->createArticleFetcher(); @@ -75,7 +74,7 @@ public function test_get_articles_from_unsupported_feed_type(): void { $feed = Feed::factory()->create([ '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(); @@ -88,7 +87,7 @@ public function test_get_articles_from_unsupported_feed_type(): void public function test_fetch_article_data_returns_array(): void { $article = Article::factory()->create([ - 'url' => 'https://example.com/article' + 'url' => 'https://example.com/article', ]); $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 { $article = Article::factory()->create([ - 'url' => 'invalid-url' + 'url' => 'invalid-url', ]); $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 $feed = Feed::factory()->create([ 'type' => 'website', - 'url' => 'https://example.com/feed' + 'url' => 'https://example.com/feed', ]); // 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 Http::fake([ - 'https://www.vrt.be/vrtnws/nl/' => Http::response('Sample VRT content', 200) + 'https://www.vrt.be/vrtnws/nl/' => Http::response('Sample VRT content', 200), ]); $feed = Feed::factory()->create([ 'type' => 'website', - 'url' => 'https://www.vrt.be/vrtnws/nl/' + 'url' => 'https://www.vrt.be/vrtnws/nl/', ]); // 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([ '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(); @@ -175,11 +174,11 @@ public function test_fetch_article_data_with_supported_parser(): void { // Mock successful HTTP response with sample HTML Http::fake([ - 'https://www.vrt.be/vrtnws/nl/test-article' => Http::response('Sample article content', 200) + 'https://www.vrt.be/vrtnws/nl/test-article' => Http::response('Sample article content', 200), ]); $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 @@ -193,7 +192,7 @@ public function test_fetch_article_data_with_supported_parser(): void public function test_fetch_article_data_handles_unsupported_domain(): void { $article = Article::factory()->create([ - 'url' => 'https://unsupported-domain.com/article' + 'url' => 'https://unsupported-domain.com/article', ]); $articleFetcher = $this->createArticleFetcher(); @@ -230,7 +229,7 @@ public function test_save_article_returns_existing_article_when_exists(): void $feed = Feed::factory()->create(); $existingArticle = Article::factory()->create([ 'url' => 'https://example.com/existing-article', - 'feed_id' => $feed->id + 'feed_id' => $feed->id, ]); // Use reflection to access private method for testing diff --git a/tests/Unit/Services/Auth/LemmyAuthServiceTest.php b/tests/Unit/Services/Auth/LemmyAuthServiceTest.php index d77f46b..d6ec76f 100644 --- a/tests/Unit/Services/Auth/LemmyAuthServiceTest.php +++ b/tests/Unit/Services/Auth/LemmyAuthServiceTest.php @@ -13,7 +13,7 @@ class LemmyAuthServiceTest extends TestCase { use RefreshDatabase; - + protected function setUp(): void { parent::setUp(); @@ -24,13 +24,13 @@ public function test_get_token_successfully_authenticates(): void // Mock successful HTTP response for both HTTPS and HTTP (fallback) Http::fake([ '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([ 'username' => 'testuser', 'password' => 'testpass', - 'instance_url' => 'https://lemmy.test' + 'instance_url' => 'https://lemmy.test', ]); $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 Http::fake([ '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); @@ -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 Http::fake([ '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); @@ -159,4 +159,4 @@ public function test_platform_auth_exception_contains_correct_platform(): void $this->assertEquals(PlatformEnum::LEMMY, $e->getPlatform()); } } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/DashboardStatsServiceTest.php b/tests/Unit/Services/DashboardStatsServiceTest.php index d8cfc4b..d84b379 100644 --- a/tests/Unit/Services/DashboardStatsServiceTest.php +++ b/tests/Unit/Services/DashboardStatsServiceTest.php @@ -3,24 +3,24 @@ namespace Tests\Unit\Services; use App\Services\DashboardStatsService; -use Tests\TestCase; use Illuminate\Support\Facades\Http; +use Tests\TestCase; class DashboardStatsServiceTest extends TestCase { protected function setUp(): void { parent::setUp(); - + // Mock HTTP requests to prevent external calls Http::fake([ - '*' => Http::response('', 500) + '*' => Http::response('', 500), ]); } public function test_get_available_periods_returns_correct_options(): void { - $service = new DashboardStatsService(); + $service = new DashboardStatsService; $periods = $service->getAvailablePeriods(); $this->assertIsArray($periods); @@ -29,14 +29,14 @@ public function test_get_available_periods_returns_correct_options(): void $this->assertArrayHasKey('month', $periods); $this->assertArrayHasKey('year', $periods); $this->assertArrayHasKey('all', $periods); - + $this->assertEquals('Today', $periods['today']); $this->assertEquals('All Time', $periods['all']); } public function test_service_instantiation(): void { - $service = new DashboardStatsService(); + $service = new DashboardStatsService; $this->assertInstanceOf(DashboardStatsService::class, $service); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Factories/ArticleParserFactoryTest.php b/tests/Unit/Services/Factories/ArticleParserFactoryTest.php index ef5a24a..23a5d42 100644 --- a/tests/Unit/Services/Factories/ArticleParserFactoryTest.php +++ b/tests/Unit/Services/Factories/ArticleParserFactoryTest.php @@ -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 { // Create a mock parser class - $mockParserClass = new class implements ArticleParserInterface { + $mockParserClass = new class implements ArticleParserInterface + { public function canParse(string $url): bool { return str_contains($url, 'test-parser.com'); @@ -82,7 +83,7 @@ public function getSourceName(): string }; $mockParserClassName = get_class($mockParserClass); - + // Register the mock parser ArticleParserFactory::registerParser($mockParserClassName); @@ -115,7 +116,8 @@ public function test_register_parser_prevents_duplicate_registration(): void public function test_get_parser_uses_first_matching_parser(): void { // 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 { 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 { return str_contains($url, 'shared-domain.com'); @@ -159,7 +162,7 @@ public function getSourceName(): string // The first registered parser should be returned $testUrl = 'https://shared-domain.com/article'; $parser = ArticleParserFactory::getParser($testUrl); - + // Should return the first parser since it was registered first $this->assertInstanceOf($mockParser1Class, $parser); } @@ -167,7 +170,8 @@ public function getSourceName(): string public function test_factory_maintains_parser_registration_across_calls(): void { // Create a mock parser - $mockParser = new class implements ArticleParserInterface { + $mockParser = new class implements ArticleParserInterface + { public function canParse(string $url): bool { return str_contains($url, 'persistent-test.com'); @@ -185,7 +189,7 @@ public function getSourceName(): string }; $mockParserClass = get_class($mockParser); - + // Register the parser ArticleParserFactory::registerParser($mockParserClass); @@ -195,7 +199,7 @@ public function getSourceName(): string $this->assertInstanceOf($mockParserClass, $parser1); $this->assertInstanceOf($mockParserClass, $parser2); - + // Verify both instances are of the same class but different objects $this->assertEquals(get_class($parser1), get_class($parser2)); } @@ -216,11 +220,11 @@ public function test_get_supported_sources_creates_new_instances_for_each_call() { // This test ensures that getSupportedSources doesn't cause issues // by creating new instances each time it's called - + $sources1 = ArticleParserFactory::getSupportedSources(); $sources2 = ArticleParserFactory::getSupportedSources(); $this->assertEquals($sources1, $sources2); $this->assertCount(count($sources1), $sources2); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Http/HttpFetcherTest.php b/tests/Unit/Services/Http/HttpFetcherTest.php index be62d67..7f6c8c9 100644 --- a/tests/Unit/Services/Http/HttpFetcherTest.php +++ b/tests/Unit/Services/Http/HttpFetcherTest.php @@ -24,7 +24,7 @@ public function test_fetch_html_returns_response_body_on_successful_request(): v $expectedHtml = 'Test content'; Http::fake([ - $url => Http::response($expectedHtml, 200) + $url => Http::response($expectedHtml, 200), ]); $result = HttpFetcher::fetchHtml($url); @@ -41,7 +41,7 @@ public function test_fetch_html_throws_exception_on_unsuccessful_response(): voi $statusCode = 404; Http::fake([ - $url => Http::response('Not Found', $statusCode) + $url => Http::response('Not Found', $statusCode), ]); $this->expectException(Exception::class); @@ -55,7 +55,7 @@ public function test_fetch_html_logs_error_on_exception(): void $url = 'https://example.com'; Http::fake([ - $url => Http::response('Server Error', 500) + $url => Http::response('Server Error', 500), ]); try { @@ -87,7 +87,7 @@ public function test_fetch_multiple_urls_returns_successful_results(): void { $urls = [ 'https://example.com/page1', - 'https://example.com/page2' + 'https://example.com/page2', ]; $html1 = 'Page 1'; @@ -95,23 +95,23 @@ public function test_fetch_multiple_urls_returns_successful_results(): void Http::fake([ '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); $this->assertCount(2, $results); - + $this->assertEquals([ 'url' => 'https://example.com/page1', 'html' => $html1, - 'success' => true + 'success' => true, ], $results[0]); - + $this->assertEquals([ 'url' => 'https://example.com/page2', 'html' => $html2, - 'success' => true + 'success' => true, ], $results[1]); } @@ -119,33 +119,33 @@ public function test_fetch_multiple_urls_handles_mixed_success_failure(): void { $urls = [ 'https://example.com/success', - 'https://example.com/failure' + 'https://example.com/failure', ]; $successHtml = 'Success'; Http::fake([ '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); $this->assertCount(2, $results); - + // First URL should succeed $this->assertEquals([ 'url' => 'https://example.com/success', 'html' => $successHtml, - 'success' => true + 'success' => true, ], $results[0]); - + // Second URL should fail $this->assertEquals([ 'url' => 'https://example.com/failure', 'html' => null, 'success' => false, - 'status' => 404 + 'status' => 404, ], $results[1]); } @@ -160,7 +160,7 @@ public function test_fetch_multiple_urls_returns_empty_array_on_exception(): voi $results = HttpFetcher::fetchMultipleUrls($urls); $this->assertEquals([], $results); - + // Skip log assertion as it's complex to test with logger() function } @@ -181,10 +181,11 @@ public function test_fetch_multiple_urls_handles_response_exception(): void Http::fake([ 'https://example.com' => function () { $response = Http::response('Success', 200); + // We can't easily mock an exception on the response object itself // so we'll test this scenario differently return $response; - } + }, ]); $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 $urls = [ 'https://example.com/page1', - 'https://example.com/page2' + 'https://example.com/page2', ]; Http::fake([ 'https://example.com/page1' => Http::response('Page 1', 200), - 'https://example.com/page2' => Http::response('Page 2', 200) + 'https://example.com/page2' => Http::response('Page 2', 200), ]); $results = HttpFetcher::fetchMultipleUrls($urls); @@ -223,19 +224,20 @@ public function test_fetch_html_with_various_status_codes(int $statusCode): void $url = 'https://example.com'; Http::fake([ - $url => Http::response('Error', $statusCode) + $url => Http::response('Error', $statusCode), ]); $this->expectException(Exception::class); $this->expectExceptionMessage("Status: {$statusCode}"); - + HttpFetcher::fetchHtml($url); } + /** @return array */ public static function statusCodesProvider(): array { 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 = [ 'https://example.com/first', 'https://example.com/second', - 'https://example.com/third' + 'https://example.com/third', ]; Http::fake([ 'https://example.com/first' => Http::response('First', 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); @@ -264,9 +266,9 @@ public function test_fetch_multiple_urls_preserves_url_order(): void public function test_fetch_html_logs_correct_error_information(): void { $url = 'https://example.com/test-page'; - + Http::fake([ - $url => Http::response('Forbidden', 403) + $url => Http::response('Forbidden', 403), ]); try { @@ -278,4 +280,4 @@ public function test_fetch_html_logs_correct_error_information(): void // Skip log assertion as service uses logger() function which is harder to test $this->assertTrue(true); // Just verify we get here } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Log/LogSaverTest.php b/tests/Unit/Services/Log/LogSaverTest.php index d8dc159..75c33b0 100644 --- a/tests/Unit/Services/Log/LogSaverTest.php +++ b/tests/Unit/Services/Log/LogSaverTest.php @@ -14,13 +14,13 @@ class LogSaverTest extends TestCase { use RefreshDatabase; - + private LogSaver $logSaver; - + protected function setUp(): void { parent::setUp(); - $this->logSaver = new LogSaver(); + $this->logSaver = new LogSaver; } 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([ 'platform' => PlatformEnum::LEMMY, - 'url' => 'https://lemmy.example.com' + 'url' => 'https://lemmy.example.com', ]); $channel = PlatformChannel::factory()->create([ 'name' => 'Test Channel', - 'platform_instance_id' => $platformInstance->id + 'platform_instance_id' => $platformInstance->id, ]); $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([ 'platform' => PlatformEnum::LEMMY, - 'url' => 'https://test.lemmy.com' + 'url' => 'https://test.lemmy.com', ]); $channel = PlatformChannel::factory()->create([ 'name' => 'Empty Context Channel', - 'platform_instance_id' => $platformInstance->id + 'platform_instance_id' => $platformInstance->id, ]); $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([ 'platform' => PlatformEnum::LEMMY, - 'url' => 'https://merge.lemmy.com' + 'url' => 'https://merge.lemmy.com', ]); $channel = PlatformChannel::factory()->create([ 'name' => 'Merge Test Channel', - 'platform_instance_id' => $platformInstance->id + 'platform_instance_id' => $platformInstance->id, ]); $originalContext = [ 'article_id' => 123, '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); @@ -238,11 +238,11 @@ public function test_log_with_complex_context_data(): void $complexContext = [ 'nested' => [ 'array' => ['value1', 'value2'], - 'object' => ['key' => 'value'] + 'object' => ['key' => 'value'], ], 'numbers' => [1, 2, 3.14], 'boolean' => true, - 'null_value' => null + 'null_value' => null, ]; $this->logSaver->debug('Complex context test', null, $complexContext); @@ -279,4 +279,4 @@ public function test_each_log_level_method_delegates_to_private_log_method(): vo $this->assertEquals($context, $log->context); } } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php b/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php index cb99b5e..83a78ba 100644 --- a/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php +++ b/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php @@ -323,12 +323,12 @@ public function test_extract_full_article_with_realistic_belga_html(): void $this->assertStringContainsString('110,000 people joined', $fullArticle); $this->assertStringContainsString('major incidents', $fullArticle); $this->assertStringContainsString('crowd dispersal', $fullArticle); - + // Should join paragraphs with double newlines $this->assertStringContainsString("\n\n", $fullArticle); - + // Should strip HTML tags $this->assertStringNotContainsString('', $fullArticle); $this->assertStringNotContainsString('', $fullArticle); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php b/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php index a0126d0..1e23370 100644 --- a/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php +++ b/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php @@ -282,4 +282,4 @@ public function test_extract_full_article_with_realistic_guardian_html(): void $this->assertStringContainsString("\n\n", $fullArticle); $this->assertStringNotContainsString('', $fullArticle); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Parsers/GuardianArticleParserTest.php b/tests/Unit/Services/Parsers/GuardianArticleParserTest.php index 51bcd43..1e3a477 100644 --- a/tests/Unit/Services/Parsers/GuardianArticleParserTest.php +++ b/tests/Unit/Services/Parsers/GuardianArticleParserTest.php @@ -13,7 +13,7 @@ class GuardianArticleParserTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->parser = new GuardianArticleParser(); + $this->parser = new GuardianArticleParser; } public function test_implements_article_parser_interface(): void @@ -60,4 +60,4 @@ public function test_extract_data_delegates_to_page_parser(): void $this->assertArrayHasKey('title', $data); $this->assertEquals('Test Title', $data['title']); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Parsers/VrtHomepageParserTest.php b/tests/Unit/Services/Parsers/VrtHomepageParserTest.php index 303f86d..6e51953 100644 --- a/tests/Unit/Services/Parsers/VrtHomepageParserTest.php +++ b/tests/Unit/Services/Parsers/VrtHomepageParserTest.php @@ -120,4 +120,4 @@ public function test_returns_empty_array_for_empty_html(): void $this->assertEmpty($urls); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Publishing/ArticlePublishingServiceTest.php b/tests/Unit/Services/Publishing/ArticlePublishingServiceTest.php index 6880c9f..4302a83 100644 --- a/tests/Unit/Services/Publishing/ArticlePublishingServiceTest.php +++ b/tests/Unit/Services/Publishing/ArticlePublishingServiceTest.php @@ -5,7 +5,6 @@ use App\Enums\PlatformEnum; use App\Exceptions\PublishException; use App\Models\Article; -use App\Models\ArticlePublication; use App\Models\Feed; use App\Models\PlatformAccount; use App\Models\PlatformChannel; @@ -19,7 +18,6 @@ use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; -use RuntimeException; use Tests\TestCase; class ArticlePublishingServiceTest extends TestCase @@ -27,6 +25,7 @@ class ArticlePublishingServiceTest extends TestCase use RefreshDatabase; protected ArticlePublishingService $service; + protected LogSaver $logSaver; protected function setUp(): void @@ -63,7 +62,7 @@ public function test_publish_to_routed_channels_returns_empty_collection_when_no $article = Article::factory()->create([ 'feed_id' => $feed->id, 'approval_status' => 'approved', - 'validated_at' => now() + 'validated_at' => now(), ]); $extractedData = ['title' => 'Test Title']; @@ -90,7 +89,7 @@ public function test_publish_to_routed_channels_skips_routes_without_active_acco 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // 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, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Attach account to channel as active $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Mock publisher via service seam @@ -166,13 +165,13 @@ public function test_publish_to_routed_channels_handles_publishing_failure_grace 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Attach account to channel as active $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // 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, 'platform_channel_id' => $channel1->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel2->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Attach accounts to channels as active $channel1->platformAccounts()->attach($account1->id, [ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $channel2->platformAccounts()->attach($account2->id, [ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $publisherDouble = \Mockery::mock(LemmyPublisher::class); @@ -266,24 +265,24 @@ public function test_publish_to_routed_channels_filters_out_failed_publications( 'feed_id' => $feed->id, 'platform_channel_id' => $channel1->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel2->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); // Attach accounts to channels as active $channel1->platformAccounts()->attach($account1->id, [ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $channel2->platformAccounts()->attach($account2->id, [ 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); $publisherDouble = \Mockery::mock(LemmyPublisher::class); diff --git a/tests/Unit/Services/Publishing/KeywordFilteringTest.php b/tests/Unit/Services/Publishing/KeywordFilteringTest.php index 0b98504..7a0b80c 100644 --- a/tests/Unit/Services/Publishing/KeywordFilteringTest.php +++ b/tests/Unit/Services/Publishing/KeywordFilteringTest.php @@ -18,16 +18,21 @@ class KeywordFilteringTest extends TestCase use RefreshDatabase; private ArticlePublishingService $service; + private Feed $feed; + private PlatformChannel $channel1; + private PlatformChannel $channel2; + private Route $route1; + private Route $route2; protected function setUp(): void { parent::setUp(); - + $logSaver = Mockery::mock(LogSaver::class); $logSaver->shouldReceive('info')->zeroOrMoreTimes(); $logSaver->shouldReceive('warning')->zeroOrMoreTimes(); @@ -37,23 +42,23 @@ protected function setUp(): void $this->feed = Feed::factory()->create(); $this->channel1 = PlatformChannel::factory()->create(); $this->channel2 = PlatformChannel::factory()->create(); - + // Create routes $this->route1 = Route::create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'is_active' => true, - 'priority' => 100 + 'priority' => 100, ]); - + $this->route2 = Route::create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel2->id, 'is_active' => true, - 'priority' => 50 + 'priority' => 50, ]); } - + protected function tearDown(): void { Mockery::close(); @@ -64,13 +69,13 @@ public function test_route_with_no_keywords_matches_all_articles(): void { $article = Article::factory()->create([ 'feed_id' => $this->feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = [ 'title' => 'Some random article', '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 @@ -90,25 +95,25 @@ public function test_route_with_keywords_matches_article_containing_keyword(): v 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'Belgium', - 'is_active' => true + 'is_active' => true, ]); Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'politics', - 'is_active' => true + 'is_active' => true, ]); $article = Article::factory()->create([ 'feed_id' => $this->feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = [ 'title' => 'Belgium announces new policy', '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 @@ -128,25 +133,25 @@ public function test_route_with_keywords_does_not_match_article_without_keywords 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'sports', - 'is_active' => true + 'is_active' => true, ]); Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'football', - 'is_active' => true + 'is_active' => true, ]); $article = Article::factory()->create([ 'feed_id' => $this->feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = [ 'title' => 'Economic news update', '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 @@ -166,31 +171,31 @@ public function test_inactive_keywords_are_ignored(): void 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'Belgium', - 'is_active' => false // Inactive + 'is_active' => false, // Inactive ]); Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'politics', - 'is_active' => true // Active + 'is_active' => true, // Active ]); $article = Article::factory()->create([ 'feed_id' => $this->feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedDataWithInactiveKeyword = [ 'title' => 'Belgium announces new policy', 'description' => 'The government makes changes', - 'full_article' => 'The Belgian government announced today...' + 'full_article' => 'The Belgian government announced today...', ]; $extractedDataWithActiveKeyword = [ 'title' => 'Political changes ahead', 'description' => 'Politics is changing', - 'full_article' => 'The political landscape is shifting...' + 'full_article' => 'The political landscape is shifting...', ]; // Use reflection to test private method @@ -211,18 +216,18 @@ public function test_keyword_matching_is_case_insensitive(): void 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'BELGIUM', - 'is_active' => true + 'is_active' => true, ]); $article = Article::factory()->create([ 'feed_id' => $this->feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = [ 'title' => 'belgium news', 'description' => 'About Belgium', - 'full_article' => 'News from belgium today...' + 'full_article' => 'News from belgium today...', ]; // 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, 'platform_channel_id' => $this->channel1->id, 'keyword' => 'title-word', - 'is_active' => true + 'is_active' => true, ]); $keywordInDescription = Keyword::factory()->create([ 'feed_id' => $this->feed->id, 'platform_channel_id' => $this->channel2->id, 'keyword' => 'desc-word', - 'is_active' => true + 'is_active' => true, ]); $article = Article::factory()->create([ 'feed_id' => $this->feed->id, - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $extractedData = [ 'title' => 'This contains title-word', '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 @@ -273,4 +278,4 @@ public function test_keywords_match_in_title_description_and_content(): void $this->assertTrue($result1, 'Should match keyword in title'); $this->assertTrue($result2, 'Should match keyword in description'); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/SystemStatusServiceTest.php b/tests/Unit/Services/SystemStatusServiceTest.php index e2d3163..cca208a 100644 --- a/tests/Unit/Services/SystemStatusServiceTest.php +++ b/tests/Unit/Services/SystemStatusServiceTest.php @@ -4,31 +4,32 @@ use App\Services\SystemStatusService; use Illuminate\Foundation\Testing\RefreshDatabase; -use Tests\TestCase; use Illuminate\Support\Facades\Http; +use Tests\TestCase; class SystemStatusServiceTest extends TestCase { use RefreshDatabase; + protected function setUp(): void { parent::setUp(); // Mock HTTP requests to prevent external calls Http::fake([ - '*' => Http::response('', 500) + '*' => Http::response('', 500), ]); } public function test_service_instantiation(): void { - $service = new SystemStatusService(); + $service = new SystemStatusService; $this->assertInstanceOf(SystemStatusService::class, $service); } public function test_get_system_status_returns_correct_structure(): void { - $service = new SystemStatusService(); + $service = new SystemStatusService; $status = $service->getSystemStatus(); $this->assertIsArray($status); diff --git a/tests/Unit/Services/ValidationServiceKeywordTest.php b/tests/Unit/Services/ValidationServiceKeywordTest.php index beaf87c..711088f 100644 --- a/tests/Unit/Services/ValidationServiceKeywordTest.php +++ b/tests/Unit/Services/ValidationServiceKeywordTest.php @@ -3,31 +3,31 @@ namespace Tests\Unit\Services; use App\Services\Article\ValidationService; -use Tests\TestCase; -use Tests\Traits\CreatesArticleFetcher; +use Mockery; use ReflectionClass; use ReflectionMethod; -use Mockery; +use Tests\TestCase; +use Tests\Traits\CreatesArticleFetcher; class ValidationServiceKeywordTest extends TestCase { use CreatesArticleFetcher; - + private ValidationService $validationService; - + protected function setUp(): void { parent::setUp(); $articleFetcher = $this->createArticleFetcher(); $this->validationService = new ValidationService($articleFetcher); } - + protected function tearDown(): void { Mockery::close(); parent::tearDown(); } - + /** * Helper method to access private validateByKeywords method */ @@ -36,6 +36,7 @@ private function getValidateByKeywordsMethod(): ReflectionMethod $reflection = new ReflectionClass($this->validationService); $method = $reflection->getMethod('validateByKeywords'); $method->setAccessible(true); + return $method; } @@ -177,21 +178,21 @@ public function test_all_keywords_are_functional(): void // Political parties and leaders 'N-VA', 'Bart De Wever', 'Frank Vandenbroucke', 'Alexander De Croo', 'Vooruit', 'Open Vld', 'CD&V', 'Vlaams Belang', 'PTB', 'PVDA', - + // Belgian locations and institutions 'Belgium', 'Belgian', 'Flanders', 'Flemish', 'Wallonia', 'Brussels', 'Antwerp', 'Ghent', 'Bruges', 'Leuven', 'Mechelen', 'Namur', 'Liège', 'Charleroi', 'parliament', 'government', 'minister', 'policy', 'law', 'legislation', - + // Common Belgian news topics 'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy', - 'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police' + 'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police', ]; foreach ($expectedKeywords as $keyword) { $testContent = "This article contains the keyword: {$keyword}."; $result = $method->invoke($this->validationService, $testContent); - + $this->assertTrue($result, "Keyword '{$keyword}' should match but didn't"); } } @@ -207,4 +208,4 @@ public function test_partial_keyword_matches_work(): void $this->assertTrue($method->invoke($this->validationService, 'Economic growth accelerated.')); $this->assertTrue($method->invoke($this->validationService, 'The European directive was implemented.')); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/ValidationServiceTest.php b/tests/Unit/Services/ValidationServiceTest.php index 97df0b9..cd9a83a 100644 --- a/tests/Unit/Services/ValidationServiceTest.php +++ b/tests/Unit/Services/ValidationServiceTest.php @@ -2,30 +2,28 @@ namespace Tests\Unit\Services; -use App\Services\Article\ArticleFetcher; -use App\Services\Article\ValidationService; -use App\Services\Log\LogSaver; use App\Models\Article; use App\Models\Feed; -use Tests\TestCase; -use Tests\Traits\CreatesArticleFetcher; +use App\Services\Article\ValidationService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Mockery; +use Tests\TestCase; +use Tests\Traits\CreatesArticleFetcher; class ValidationServiceTest extends TestCase { - use RefreshDatabase, CreatesArticleFetcher; - + use CreatesArticleFetcher, RefreshDatabase; + private ValidationService $validationService; - + protected function setUp(): void { parent::setUp(); $articleFetcher = $this->createArticleFetcher(); $this->validationService = new ValidationService($articleFetcher); } - + protected function tearDown(): void { Mockery::close(); @@ -36,14 +34,14 @@ public function test_validate_returns_article_with_validation_status(): void { // Mock HTTP requests Http::fake([ - 'https://example.com/article' => Http::response('Test content with Belgium news', 200) + 'https://example.com/article' => Http::response('Test content with Belgium news', 200), ]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'url' => 'https://example.com/article', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $result = $this->validationService->validate($article); @@ -56,14 +54,14 @@ public function test_validate_marks_article_invalid_when_missing_data(): void { // Mock HTTP requests to return HTML without article content Http::fake([ - 'https://invalid-url-without-parser.com/article' => Http::response('Empty', 200) + 'https://invalid-url-without-parser.com/article' => Http::response('Empty', 200), ]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'url' => 'https://invalid-url-without-parser.com/article', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $result = $this->validationService->validate($article); @@ -75,14 +73,14 @@ public function test_validate_with_supported_article_content(): void { // Mock HTTP requests Http::fake([ - 'https://example.com/article' => Http::response('Article content', 200) + 'https://example.com/article' => Http::response('Article content', 200), ]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'url' => 'https://example.com/article', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $result = $this->validationService->validate($article); @@ -95,14 +93,14 @@ public function test_validate_updates_article_in_database(): void { // Mock HTTP requests Http::fake([ - 'https://example.com/article' => Http::response('Article content', 200) + 'https://example.com/article' => Http::response('Article content', 200), ]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'url' => 'https://example.com/article', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $originalId = $article->id; @@ -118,14 +116,14 @@ public function test_validate_handles_article_with_existing_validation(): void { // Mock HTTP requests Http::fake([ - 'https://example.com/article' => Http::response('Article content', 200) + 'https://example.com/article' => Http::response('Article content', 200), ]); $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'url' => 'https://example.com/article', - 'approval_status' => 'approved' + 'approval_status' => 'approved', ]); $originalApprovalStatus = $article->approval_status; @@ -143,7 +141,7 @@ public function test_validate_keyword_checking_logic(): void 'https://example.com/article-about-bart-de-wever' => Http::response( '
Article about Bart De Wever and Belgian politics
', 200 - ) + ), ]); $feed = Feed::factory()->create(); @@ -152,7 +150,7 @@ public function test_validate_keyword_checking_logic(): void $article = Article::factory()->create([ 'feed_id' => $feed->id, 'url' => 'https://example.com/article-about-bart-de-wever', - 'approval_status' => 'pending' + 'approval_status' => 'pending', ]); $result = $this->validationService->validate($article);