Compare commits
9 commits
cf2fa647f5
...
ec09711a6f
| Author | SHA1 | Date | |
|---|---|---|---|
| ec09711a6f | |||
| bf96489362 | |||
| 0bb10729de | |||
| 19cbea9273 | |||
| a448c54e73 | |||
| 675802e8e5 | |||
| 6784af2ff6 | |||
| 56db303b15 | |||
| 0b2fc5004b |
176 changed files with 1989 additions and 1437 deletions
113
.forgejo/workflows/ci.yml
Normal file
113
.forgejo/workflows/ci.yml
Normal file
|
|
@ -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="<!-- ffr-ci-coverage-report -->"
|
||||||
|
|
||||||
|
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"], "<!-- ffr-ci-coverage-report -->")) {
|
||||||
|
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
|
||||||
|
|
@ -11,6 +11,7 @@ public function canParse(string $url): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract article data from HTML
|
* Extract article data from HTML
|
||||||
|
*
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public function extractData(string $html): array;
|
public function extractData(string $html): array;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ public function canParse(string $url): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract article URLs from homepage HTML
|
* Extract article URLs from homepage HTML
|
||||||
|
*
|
||||||
* @return array<int, string>
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
public function extractArticleUrls(string $html): array;
|
public function extractArticleUrls(string $html): array;
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,18 @@
|
||||||
enum PlatformEnum: string
|
enum PlatformEnum: string
|
||||||
{
|
{
|
||||||
case LEMMY = 'lemmy';
|
case LEMMY = 'lemmy';
|
||||||
|
|
||||||
|
public function channelLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::LEMMY => 'Community',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function channelLabelPlural(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::LEMMY => 'Communities',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
18
app/Events/ActionPerformed.php
Normal file
18
app/Events/ActionPerformed.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
|
||||||
|
class ActionPerformed
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $message,
|
||||||
|
public LogLevelEnum $level = LogLevelEnum::INFO,
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $context = [],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,5 @@ class ExceptionLogged
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public Log $log
|
public Log $log
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,5 @@ public function __construct(
|
||||||
public string $message,
|
public string $message,
|
||||||
/** @var array<string, mixed> */
|
/** @var array<string, mixed> */
|
||||||
public array $context = []
|
public array $context = []
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,5 @@ class NewArticleFetched
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
public function __construct(public Article $article)
|
public function __construct(public Article $article) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,4 @@
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class ChannelException extends Exception
|
class ChannelException extends Exception {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class PublishException extends Exception
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Article $article,
|
private readonly Article $article,
|
||||||
private readonly PlatformEnum|null $platform,
|
private readonly ?PlatformEnum $platform,
|
||||||
?Throwable $previous = null
|
?Throwable $previous = null
|
||||||
) {
|
) {
|
||||||
$message = "Failed to publish article #$article->id";
|
$message = "Failed to publish article #$article->id";
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,4 @@
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class RoutingException extends Exception
|
class RoutingException extends Exception {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Http\Resources\ArticleResource;
|
use App\Http\Resources\ArticleResource;
|
||||||
|
use App\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
|
||||||
|
|
||||||
class ArticlesController extends BaseController
|
class ArticlesController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ public function sendResponse(mixed $result, string $message = 'Success', int $co
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error response method
|
* Error response method
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $errorMessages
|
||||||
*/
|
*/
|
||||||
public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse
|
public function sendError(string $error, array $errorMessages = [], int $code = 400): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
@ -40,6 +42,8 @@ public function sendError(string $error, array $errorMessages = [], int $code =
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation error response method
|
* Validation error response method
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $errors
|
||||||
*/
|
*/
|
||||||
public function sendValidationError(array $errors): JsonResponse
|
public function sendValidationError(array $errors): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Services\DashboardStatsService;
|
use App\Services\DashboardStatsService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -40,7 +37,6 @@ public function stats(Request $request): JsonResponse
|
||||||
'current_period' => $period,
|
'current_period' => $period,
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
throw $e;
|
|
||||||
return $this->sendError('Failed to fetch dashboard stats: '.$e->getMessage(), [], 500);
|
return $this->sendError('Failed to fetch dashboard stats: '.$e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use InvalidArgumentException;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
class FeedsController extends BaseController
|
class FeedsController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
@ -37,7 +37,7 @@ public function index(Request $request): JsonResponse
|
||||||
'total' => $feeds->total(),
|
'total' => $feeds->total(),
|
||||||
'from' => $feeds->firstItem(),
|
'from' => $feeds->firstItem(),
|
||||||
'to' => $feeds->lastItem(),
|
'to' => $feeds->lastItem(),
|
||||||
]
|
],
|
||||||
], 'Feeds retrieved successfully.');
|
], 'Feeds retrieved successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,10 @@ public function options(): JsonResponse
|
||||||
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
||||||
|
|
||||||
// Get feed providers from config
|
// Get feed providers from config
|
||||||
$feedProviders = collect(config('feed.providers', []))
|
/** @var array<string, array<string, mixed>> $providers */
|
||||||
->filter(fn($provider) => $provider['is_active'])
|
$providers = config('feed.providers', []);
|
||||||
|
$feedProviders = collect($providers)
|
||||||
|
->filter(fn (array $provider) => $provider['is_active'])
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
return $this->sendResponse([
|
return $this->sendResponse([
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ public function store(StorePlatformAccountRequest $request, CreatePlatformAccoun
|
||||||
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
||||||
return $this->sendError($e->getMessage(), [], 429);
|
return $this->sendError($e->getMessage(), [], 429);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422);
|
return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return $this->sendError('Unable to connect to the Lemmy instance. Please check the URL and try again.', [], 422);
|
return $this->sendError('Unable to connect to the Lemmy instance. Please check the URL and try again.', [], 422);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
use App\Actions\CreateChannelAction;
|
use App\Actions\CreateChannelAction;
|
||||||
use App\Http\Requests\StorePlatformChannelRequest;
|
use App\Http\Requests\StorePlatformChannelRequest;
|
||||||
use App\Http\Resources\PlatformChannelResource;
|
use App\Http\Resources\PlatformChannelResource;
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -144,6 +144,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
|
||||||
'priority' => 'nullable|integer|min:1|max:100',
|
'priority' => 'nullable|integer|min:1|max:100',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** @var PlatformAccount $platformAccount */
|
||||||
$platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']);
|
$platformAccount = PlatformAccount::findOrFail($validated['platform_account_id']);
|
||||||
|
|
||||||
// Check if account is already attached
|
// Check if account is already attached
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Auth\Events\Verified;
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
|
@ -19,7 +20,9 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->user()->markEmailAsVerified()) {
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
event(new Verified($request->user()));
|
/** @var MustVerifyEmail $user */
|
||||||
|
$user = $request->user();
|
||||||
|
event(new Verified($user));
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public function rules(): array
|
||||||
'provider' => "required|in:{$providers}",
|
'provider' => "required|in:{$providers}",
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_id' => 'required|exists:languages,id',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ public function rules(): array
|
||||||
'type' => 'required|in:website,rss',
|
'type' => 'required|in:website,rss',
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_id' => 'required|exists:languages,id',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\ArticlePublication
|
||||||
|
*/
|
||||||
class ArticlePublicationResource extends JsonResource
|
class ArticlePublicationResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,8 +20,8 @@ public function toArray(Request $request): array
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'article_id' => $this->article_id,
|
'article_id' => $this->article_id,
|
||||||
'status' => $this->status,
|
'platform' => $this->platform,
|
||||||
'published_at' => $this->published_at?->toISOString(),
|
'published_at' => $this->published_at->toISOString(),
|
||||||
'created_at' => $this->created_at->toISOString(),
|
'created_at' => $this->created_at->toISOString(),
|
||||||
'updated_at' => $this->updated_at->toISOString(),
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @mixin \App\Models\Article
|
||||||
*/
|
*/
|
||||||
class ArticleResource extends JsonResource
|
class ArticleResource extends JsonResource
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
@ -19,12 +22,8 @@ public function toArray(Request $request): array
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'is_valid' => $this->is_valid,
|
'is_valid' => $this->is_valid,
|
||||||
'is_duplicate' => $this->is_duplicate,
|
|
||||||
'approval_status' => $this->approval_status,
|
'approval_status' => $this->approval_status,
|
||||||
'publish_status' => $this->publish_status,
|
'publish_status' => $this->publish_status,
|
||||||
'approved_at' => $this->approved_at?->toISOString(),
|
|
||||||
'approved_by' => $this->approved_by,
|
|
||||||
'fetched_at' => $this->fetched_at?->toISOString(),
|
|
||||||
'validated_at' => $this->validated_at?->toISOString(),
|
'validated_at' => $this->validated_at?->toISOString(),
|
||||||
'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null,
|
'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null,
|
||||||
'created_at' => $this->created_at->toISOString(),
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\Feed
|
||||||
|
*/
|
||||||
class FeedResource extends JsonResource
|
class FeedResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\PlatformAccount
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\PlatformAccount
|
||||||
|
*/
|
||||||
class PlatformAccountResource extends JsonResource
|
class PlatformAccountResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\PlatformChannel
|
||||||
|
*/
|
||||||
class PlatformChannelResource extends JsonResource
|
class PlatformChannelResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\PlatformInstance
|
||||||
|
*/
|
||||||
class PlatformInstanceResource extends JsonResource
|
class PlatformInstanceResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Models\Route
|
||||||
|
*/
|
||||||
class RouteResource extends JsonResource
|
class RouteResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,7 +18,6 @@ class RouteResource extends JsonResource
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
|
||||||
'feed_id' => $this->feed_id,
|
'feed_id' => $this->feed_id,
|
||||||
'platform_channel_id' => $this->platform_channel_id,
|
'platform_channel_id' => $this->platform_channel_id,
|
||||||
'is_active' => $this->is_active,
|
'is_active' => $this->is_active,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher): void
|
||||||
$logSaver->info('Starting feed article fetch', null, [
|
$logSaver->info('Starting feed article fetch', null, [
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'feed_name' => $this->feed->name,
|
'feed_name' => $this->feed->name,
|
||||||
'feed_url' => $this->feed->url
|
'feed_url' => $this->feed->url,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$articles = $articleFetcher->getArticlesFromFeed($this->feed);
|
$articles = $articleFetcher->getArticlesFromFeed($this->feed);
|
||||||
|
|
@ -33,7 +33,7 @@ public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher): void
|
||||||
$logSaver->info('Feed article fetch completed', null, [
|
$logSaver->info('Feed article fetch completed', null, [
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'feed_name' => $this->feed->name,
|
'feed_name' => $this->feed->name,
|
||||||
'articles_count' => $articles->count()
|
'articles_count' => $articles->count(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->feed->update(['last_fetched_at' => now()]);
|
$this->feed->update(['last_fetched_at' => now()]);
|
||||||
|
|
@ -56,7 +56,7 @@ public static function dispatchForAllActiveFeeds(): void
|
||||||
$logSaver->info('Dispatched feed discovery job', null, [
|
$logSaver->info('Dispatched feed discovery job', null, [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'feed_name' => $feed->name,
|
'feed_name' => $feed->name,
|
||||||
'delay_minutes' => $delayMinutes
|
'delay_minutes' => $delayMinutes,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Exceptions\PublishException;
|
use App\Exceptions\PublishException;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\ArticlePublication;
|
use App\Models\ArticlePublication;
|
||||||
|
|
@ -12,7 +14,7 @@
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
class PublishNextArticleJob implements ShouldQueue, ShouldBeUnique
|
class PublishNextArticleJob implements ShouldBeUnique, ShouldQueue
|
||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
|
|
@ -28,6 +30,7 @@ public function __construct()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
*
|
||||||
* @throws PublishException
|
* @throws PublishException
|
||||||
*/
|
*/
|
||||||
public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService $publishingService): void
|
public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService $publishingService): void
|
||||||
|
|
@ -52,11 +55,11 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger()->info('Publishing next article from scheduled job', [
|
ActionPerformed::dispatch('Publishing next article from scheduled job', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
'url' => $article->url,
|
'url' => $article->url,
|
||||||
'created_at' => $article->created_at
|
'created_at' => $article->created_at,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Fetch article data
|
// Fetch article data
|
||||||
|
|
@ -65,14 +68,14 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService
|
||||||
try {
|
try {
|
||||||
$publishingService->publishToRoutedChannels($article, $extractedData);
|
$publishingService->publishToRoutedChannels($article, $extractedData);
|
||||||
|
|
||||||
logger()->info('Successfully published article', [
|
ActionPerformed::dispatch('Successfully published article', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
} catch (PublishException $e) {
|
} catch (PublishException $e) {
|
||||||
logger()->error('Failed to publish article', [
|
ActionPerformed::dispatch('Failed to publish article', LogLevelEnum::ERROR, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class SyncChannelPostsJob implements ShouldQueue, ShouldBeUnique
|
class SyncChannelPostsJob implements ShouldBeUnique, ShouldQueue
|
||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ private function syncLemmyChannelPosts(LogSaver $logSaver): void
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$logSaver->error('Failed to sync channel posts', $this->channel, [
|
$logSaver->error('Failed to sync channel posts', $this->channel, [
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|
|
||||||
21
app/Listeners/LogActionListener.php
Normal file
21
app/Listeners/LogActionListener.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
|
use App\Services\Log\LogSaver;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class LogActionListener
|
||||||
|
{
|
||||||
|
public function __construct(private LogSaver $logSaver) {}
|
||||||
|
|
||||||
|
public function handle(ActionPerformed $event): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->logSaver->log($event->level, $event->message, context: $event->context);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Failed to log action to database: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
use App\Events\ExceptionLogged;
|
use App\Events\ExceptionLogged;
|
||||||
use App\Events\ExceptionOccurred;
|
use App\Events\ExceptionOccurred;
|
||||||
use App\Models\Log;
|
use App\Models\Log;
|
||||||
|
|
||||||
class LogExceptionToDatabase
|
class LogExceptionToDatabase
|
||||||
{
|
{
|
||||||
|
|
||||||
public function handle(ExceptionOccurred $event): void
|
public function handle(ExceptionOccurred $event): void
|
||||||
{
|
{
|
||||||
// Truncate the message to prevent database errors
|
// Truncate the message to prevent database errors
|
||||||
|
|
@ -24,15 +24,15 @@ public function handle(ExceptionOccurred $event): void
|
||||||
'file' => $event->exception->getFile(),
|
'file' => $event->exception->getFile(),
|
||||||
'line' => $event->exception->getLine(),
|
'line' => $event->exception->getLine(),
|
||||||
'trace' => $event->exception->getTraceAsString(),
|
'trace' => $event->exception->getTraceAsString(),
|
||||||
...$event->context
|
...$event->context,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ExceptionLogged::dispatch($log);
|
ExceptionLogged::dispatch($log);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Prevent infinite recursion by not logging this exception
|
// Prevent infinite recursion by not logging this exception
|
||||||
// Optionally log to file or other non-database destination
|
// 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ArticleApproved;
|
use App\Events\ArticleApproved;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use App\Services\Article\ArticleFetcher;
|
||||||
use App\Services\Publishing\ArticlePublishingService;
|
use App\Services\Publishing\ArticlePublishingService;
|
||||||
|
|
@ -40,14 +42,14 @@ public function handle(ArticleApproved $event): void
|
||||||
if ($publications->isNotEmpty()) {
|
if ($publications->isNotEmpty()) {
|
||||||
$article->update(['publish_status' => 'published']);
|
$article->update(['publish_status' => 'published']);
|
||||||
|
|
||||||
logger()->info('Published approved article', [
|
ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$article->update(['publish_status' => 'error']);
|
$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,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
|
|
@ -55,7 +57,7 @@ public function handle(ArticleApproved $event): void
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$article->update(['publish_status' => 'error']);
|
$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,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\NewArticleFetched;
|
use App\Events\NewArticleFetched;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Services\Article\ValidationService;
|
use App\Services\Article\ValidationService;
|
||||||
|
|
@ -37,10 +39,11 @@ public function handle(NewArticleFetched $event): void
|
||||||
try {
|
try {
|
||||||
$article = $this->validationService->validate($article);
|
$article = $this->validationService->validate($article);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Article validation failed', [
|
ActionPerformed::dispatch('Article validation failed', LogLevelEnum::ERROR, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ public function refresh(): void
|
||||||
$this->dispatch('refresh-started');
|
$this->dispatch('refresh-started');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
$articles = Article::with(['feed', 'articlePublication'])
|
$articles = Article::with(['feed', 'articlePublication'])
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ public function detachAccount(int $channelId, int $accountId): void
|
||||||
$channel->platformAccounts()->detach($accountId);
|
$channel->platformAccounts()->detach($accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
$channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get();
|
$channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get();
|
||||||
$allAccounts = PlatformAccount::where('is_active', true)->get();
|
$allAccounts = PlatformAccount::where('is_active', true)->get();
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public function setPeriod(string $period): void
|
||||||
$this->period = $period;
|
$this->period = $period;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
$service = app(DashboardStatsService::class);
|
$service = app(DashboardStatsService::class);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ public function toggle(int $feedId): void
|
||||||
$feed->save();
|
$feed->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
$feeds = Feed::orderBy('name')->get();
|
$feeds = Feed::orderBy('name')->get();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,36 +29,54 @@ class Onboarding extends Component
|
||||||
|
|
||||||
// Platform form
|
// Platform form
|
||||||
public string $instanceUrl = '';
|
public string $instanceUrl = '';
|
||||||
|
|
||||||
public string $username = '';
|
public string $username = '';
|
||||||
|
|
||||||
public string $password = '';
|
public string $password = '';
|
||||||
|
|
||||||
|
/** @var array<string, mixed>|null */
|
||||||
public ?array $existingAccount = null;
|
public ?array $existingAccount = null;
|
||||||
|
|
||||||
// Feed form
|
// Feed form
|
||||||
public string $feedName = '';
|
public string $feedName = '';
|
||||||
|
|
||||||
public string $feedProvider = 'vrt';
|
public string $feedProvider = 'vrt';
|
||||||
|
|
||||||
public ?int $feedLanguageId = null;
|
public ?int $feedLanguageId = null;
|
||||||
|
|
||||||
public string $feedDescription = '';
|
public string $feedDescription = '';
|
||||||
|
|
||||||
// Channel form
|
// Channel form
|
||||||
public string $channelName = '';
|
public string $channelName = '';
|
||||||
|
|
||||||
public ?int $platformInstanceId = null;
|
public ?int $platformInstanceId = null;
|
||||||
|
|
||||||
public ?int $channelLanguageId = null;
|
public ?int $channelLanguageId = null;
|
||||||
|
|
||||||
public string $channelDescription = '';
|
public string $channelDescription = '';
|
||||||
|
|
||||||
// Route form
|
// Route form
|
||||||
public ?int $routeFeedId = null;
|
public ?int $routeFeedId = null;
|
||||||
|
|
||||||
public ?int $routeChannelId = null;
|
public ?int $routeChannelId = null;
|
||||||
|
|
||||||
public int $routePriority = 50;
|
public int $routePriority = 50;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
/** @var array<string, string> */
|
||||||
public array $formErrors = [];
|
public array $formErrors = [];
|
||||||
|
|
||||||
public bool $isLoading = false;
|
public bool $isLoading = false;
|
||||||
|
|
||||||
#[\Livewire\Attributes\Locked]
|
#[\Livewire\Attributes\Locked]
|
||||||
public ?int $previousChannelLanguageId = null;
|
public ?int $previousChannelLanguageId = null;
|
||||||
|
|
||||||
protected CreatePlatformAccountAction $createPlatformAccountAction;
|
protected CreatePlatformAccountAction $createPlatformAccountAction;
|
||||||
|
|
||||||
protected CreateFeedAction $createFeedAction;
|
protected CreateFeedAction $createFeedAction;
|
||||||
|
|
||||||
protected CreateChannelAction $createChannelAction;
|
protected CreateChannelAction $createChannelAction;
|
||||||
|
|
||||||
protected CreateRouteAction $createRouteAction;
|
protected CreateRouteAction $createRouteAction;
|
||||||
|
|
||||||
public function boot(
|
public function boot(
|
||||||
|
|
@ -319,6 +337,9 @@ public function completeOnboarding(): void
|
||||||
/**
|
/**
|
||||||
* Get language codes that have at least one active provider.
|
* Get language codes that have at least one active provider.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
public function getAvailableLanguageCodes(): array
|
public function getAvailableLanguageCodes(): array
|
||||||
{
|
{
|
||||||
$providers = config('feed.providers', []);
|
$providers = config('feed.providers', []);
|
||||||
|
|
@ -329,7 +350,7 @@ public function getAvailableLanguageCodes(): array
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
foreach (array_keys($provider['languages'] ?? []) as $code) {
|
foreach (array_keys($provider['languages'] ?? []) as $code) {
|
||||||
$languageCodes[$code] = true;
|
$languageCodes[(string) $code] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,6 +360,9 @@ public function getAvailableLanguageCodes(): array
|
||||||
/**
|
/**
|
||||||
* Get providers available for the current channel language.
|
* Get providers available for the current channel language.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, string>>
|
||||||
|
*/
|
||||||
public function getProvidersForLanguage(): array
|
public function getProvidersForLanguage(): array
|
||||||
{
|
{
|
||||||
if (! $this->channelLanguageId) {
|
if (! $this->channelLanguageId) {
|
||||||
|
|
@ -378,10 +402,11 @@ public function getChannelLanguage(): ?Language
|
||||||
if (! $this->channelLanguageId) {
|
if (! $this->channelLanguageId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Language::find($this->channelLanguageId);
|
return Language::find($this->channelLanguageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
// For channel step: only show languages that have providers
|
// For channel step: only show languages that have providers
|
||||||
$availableCodes = $this->getAvailableLanguageCodes();
|
$availableCodes = $this->getAvailableLanguageCodes();
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,16 @@
|
||||||
class Routes extends Component
|
class Routes extends Component
|
||||||
{
|
{
|
||||||
public bool $showCreateModal = false;
|
public bool $showCreateModal = false;
|
||||||
|
|
||||||
public ?int $editingFeedId = null;
|
public ?int $editingFeedId = null;
|
||||||
|
|
||||||
public ?int $editingChannelId = null;
|
public ?int $editingChannelId = null;
|
||||||
|
|
||||||
// Create form
|
// Create form
|
||||||
public ?int $newFeedId = null;
|
public ?int $newFeedId = null;
|
||||||
|
|
||||||
public ?int $newChannelId = null;
|
public ?int $newChannelId = null;
|
||||||
|
|
||||||
public int $newPriority = 50;
|
public int $newPriority = 50;
|
||||||
|
|
||||||
// Edit form
|
// Edit form
|
||||||
|
|
@ -24,6 +28,7 @@ class Routes extends Component
|
||||||
|
|
||||||
// Keyword management
|
// Keyword management
|
||||||
public string $newKeyword = '';
|
public string $newKeyword = '';
|
||||||
|
|
||||||
public bool $showKeywordInput = false;
|
public bool $showKeywordInput = false;
|
||||||
|
|
||||||
public function openCreateModal(): void
|
public function openCreateModal(): void
|
||||||
|
|
@ -53,6 +58,7 @@ public function createRoute(): void
|
||||||
|
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
$this->addError('newFeedId', 'This route already exists.');
|
$this->addError('newFeedId', 'This route already exists.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,9 +159,9 @@ public function deleteKeyword(int $keywordId): void
|
||||||
Keyword::destroy($keywordId);
|
Keyword::destroy($keywordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
$routes = Route::with(['feed', 'platformChannel'])
|
$routes = Route::with(['feed', 'platformChannel.platformInstance'])
|
||||||
->orderBy('priority', 'desc')
|
->orderBy('priority', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|
@ -168,7 +174,8 @@ public function render()
|
||||||
|
|
||||||
$routes = $routes->map(function ($route) use ($allKeywords) {
|
$routes = $routes->map(function ($route) use ($allKeywords) {
|
||||||
$key = $route->feed_id.'-'.$route->platform_channel_id;
|
$key = $route->feed_id.'-'.$route->platform_channel_id;
|
||||||
$route->keywords = $allKeywords->get($key, collect());
|
$route->setRelation('keywords', $allKeywords->get($key, collect()));
|
||||||
|
|
||||||
return $route;
|
return $route;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -179,7 +186,7 @@ public function render()
|
||||||
$editingKeywords = collect();
|
$editingKeywords = collect();
|
||||||
|
|
||||||
if ($this->editingFeedId && $this->editingChannelId) {
|
if ($this->editingFeedId && $this->editingChannelId) {
|
||||||
$editingRoute = Route::with(['feed', 'platformChannel'])
|
$editingRoute = Route::with(['feed', 'platformChannel.platformInstance'])
|
||||||
->where('feed_id', $this->editingFeedId)
|
->where('feed_id', $this->editingFeedId)
|
||||||
->where('platform_channel_id', $this->editingChannelId)
|
->where('platform_channel_id', $this->editingChannelId)
|
||||||
->first();
|
->first();
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@
|
||||||
class Settings extends Component
|
class Settings extends Component
|
||||||
{
|
{
|
||||||
public bool $articleProcessingEnabled = true;
|
public bool $articleProcessingEnabled = true;
|
||||||
|
|
||||||
public bool $publishingApprovalsEnabled = false;
|
public bool $publishingApprovalsEnabled = false;
|
||||||
|
|
||||||
public int $articlePublishingInterval = 5;
|
public int $articlePublishingInterval = 5;
|
||||||
|
|
||||||
public ?string $successMessage = null;
|
public ?string $successMessage = null;
|
||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
|
|
@ -60,7 +63,7 @@ public function clearMessages(): void
|
||||||
$this->errorMessage = null;
|
$this->errorMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
return view('livewire.settings')->layout('layouts.app');
|
return view('livewire.settings')->layout('layouts.app');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,20 @@
|
||||||
* @method static firstOrCreate(array<string, mixed> $array)
|
* @method static firstOrCreate(array<string, mixed> $array)
|
||||||
* @method static where(string $string, string $url)
|
* @method static where(string $string, string $url)
|
||||||
* @method static create(array<string, mixed> $array)
|
* @method static create(array<string, mixed> $array)
|
||||||
* @property integer $id
|
*
|
||||||
|
* @property int $id
|
||||||
* @property int $feed_id
|
* @property int $feed_id
|
||||||
* @property Feed $feed
|
* @property Feed $feed
|
||||||
* @property string $url
|
* @property string $url
|
||||||
|
* @property string $title
|
||||||
|
* @property string|null $description
|
||||||
|
* @property string $approval_status
|
||||||
|
* @property string $publish_status
|
||||||
* @property bool|null $is_valid
|
* @property bool|null $is_valid
|
||||||
* @property Carbon|null $validated_at
|
* @property Carbon|null $validated_at
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
* @property ArticlePublication $articlePublication
|
* @property ArticlePublication|null $articlePublication
|
||||||
*/
|
*/
|
||||||
class Article extends Model
|
class Article extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -79,7 +84,7 @@ public function isRejected(): bool
|
||||||
return $this->approval_status === 'rejected';
|
return $this->approval_status === 'rejected';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function approve(string $approvedBy = null): void
|
public function approve(?string $approvedBy = null): void
|
||||||
{
|
{
|
||||||
$this->update([
|
$this->update([
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
|
|
@ -89,7 +94,7 @@ public function approve(string $approvedBy = null): void
|
||||||
event(new ArticleApproved($this));
|
event(new ArticleApproved($this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reject(string $rejectedBy = null): void
|
public function reject(?string $rejectedBy = null): void
|
||||||
{
|
{
|
||||||
$this->update([
|
$this->update([
|
||||||
'approval_status' => 'rejected',
|
'approval_status' => 'rejected',
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,16 @@
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property integer $article_id
|
* @property int $id
|
||||||
* @property integer $platform_channel_id
|
* @property int $article_id
|
||||||
* @property integer $post_id
|
* @property int $platform_channel_id
|
||||||
|
* @property string $post_id
|
||||||
|
* @property string $platform
|
||||||
|
* @property string $published_by
|
||||||
|
* @property array<string, mixed>|null $publication_data
|
||||||
|
* @property \Illuminate\Support\Carbon $published_at
|
||||||
|
* @property \Illuminate\Support\Carbon $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon $updated_at
|
||||||
*
|
*
|
||||||
* @method static create(array<string, mixed> $array)
|
* @method static create(array<string, mixed> $array)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property string $provider
|
* @property string $provider
|
||||||
* @property int $language_id
|
* @property int|null $language_id
|
||||||
* @property Language|null $language
|
* @property Language|null $language
|
||||||
* @property string $description
|
* @property string $description
|
||||||
* @property array<string, mixed> $settings
|
* @property array<string, mixed> $settings
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
* @property Carbon|null $last_fetched_at
|
* @property Carbon|null $last_fetched_at
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
|
*
|
||||||
* @method static orderBy(string $string, string $string1)
|
* @method static orderBy(string $string, string $string1)
|
||||||
* @method static where(string $string, true $true)
|
* @method static where(string $string, true $true)
|
||||||
* @method static findOrFail(mixed $feed_id)
|
* @method static findOrFail(mixed $feed_id)
|
||||||
|
|
@ -32,7 +33,9 @@ class Feed extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<FeedFactory> */
|
/** @use HasFactory<FeedFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
private const RECENT_FETCH_THRESHOLD_HOURS = 2;
|
private const RECENT_FETCH_THRESHOLD_HOURS = 2;
|
||||||
|
|
||||||
private const DAILY_FETCH_THRESHOLD_HOURS = 24;
|
private const DAILY_FETCH_THRESHOLD_HOURS = 24;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|
@ -44,13 +47,13 @@ class Feed extends Model
|
||||||
'description',
|
'description',
|
||||||
'settings',
|
'settings',
|
||||||
'is_active',
|
'is_active',
|
||||||
'last_fetched_at'
|
'last_fetched_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'settings' => 'array',
|
'settings' => 'array',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'last_fetched_at' => 'datetime'
|
'last_fetched_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getTypeDisplayAttribute(): string
|
public function getTypeDisplayAttribute(): string
|
||||||
|
|
@ -79,12 +82,12 @@ public function getStatusAttribute(): string
|
||||||
} elseif ($hoursAgo < self::DAILY_FETCH_THRESHOLD_HOURS) {
|
} elseif ($hoursAgo < self::DAILY_FETCH_THRESHOLD_HOURS) {
|
||||||
return "Fetched {$hoursAgo}h ago";
|
return "Fetched {$hoursAgo}h ago";
|
||||||
} else {
|
} else {
|
||||||
return "Fetched " . $this->last_fetched_at->diffForHumans();
|
return 'Fetched '.$this->last_fetched_at->diffForHumans();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<PlatformChannel, $this, Route>
|
* @return BelongsToMany<PlatformChannel, $this>
|
||||||
*/
|
*/
|
||||||
public function channels(): BelongsToMany
|
public function channels(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|
@ -94,7 +97,7 @@ public function channels(): BelongsToMany
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<PlatformChannel, $this, Route>
|
* @return BelongsToMany<PlatformChannel, $this>
|
||||||
*/
|
*/
|
||||||
public function activeChannels(): BelongsToMany
|
public function activeChannels(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\KeywordFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -20,17 +21,18 @@
|
||||||
*/
|
*/
|
||||||
class Keyword extends Model
|
class Keyword extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<KeywordFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
'keyword',
|
'keyword',
|
||||||
'is_active'
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,5 +50,4 @@ public function platformChannel(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(PlatformChannel::class);
|
return $this->belongsTo(PlatformChannel::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ class Language extends Model
|
||||||
'short_code',
|
'short_code',
|
||||||
'name',
|
'name',
|
||||||
'native_name',
|
'native_name',
|
||||||
'is_active'
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use App\Enums\LogLevelEnum;
|
||||||
|
use Database\Factories\LogFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static create(array $array)
|
* @method static create(array<string, mixed> $array)
|
||||||
|
*
|
||||||
* @property LogLevelEnum $level
|
* @property LogLevelEnum $level
|
||||||
* @property string $message
|
* @property string $message
|
||||||
* @property array<string, mixed> $context
|
* @property array<string, mixed> $context
|
||||||
|
|
@ -17,6 +19,7 @@
|
||||||
*/
|
*/
|
||||||
class Log extends Model
|
class Log extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<LogFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'logs';
|
protected $table = 'logs';
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\PlatformEnum;
|
||||||
use Database\Factories\PlatformAccountFactory;
|
use Database\Factories\PlatformAccountFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use App\Enums\PlatformEnum;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
|
|
@ -18,13 +18,14 @@
|
||||||
* @property string $instance_url
|
* @property string $instance_url
|
||||||
* @property string $username
|
* @property string $username
|
||||||
* @property string $password
|
* @property string $password
|
||||||
* @property string $settings
|
* @property array<string, mixed> $settings
|
||||||
* @property bool $is_active
|
* @property bool $is_active
|
||||||
* @property Carbon $last_tested_at
|
* @property Carbon|null $last_tested_at
|
||||||
* @property string $status
|
* @property string $status
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
* @property Collection<int, PlatformChannel> $activeChannels
|
* @property Collection<int, PlatformChannel> $activeChannels
|
||||||
|
*
|
||||||
* @method static where(string $string, PlatformEnum $platform)
|
* @method static where(string $string, PlatformEnum $platform)
|
||||||
* @method static orderBy(string $string)
|
* @method static orderBy(string $string)
|
||||||
* @method static create(array<string, mixed> $validated)
|
* @method static create(array<string, mixed> $validated)
|
||||||
|
|
@ -42,14 +43,14 @@ class PlatformAccount extends Model
|
||||||
'settings',
|
'settings',
|
||||||
'is_active',
|
'is_active',
|
||||||
'last_tested_at',
|
'last_tested_at',
|
||||||
'status'
|
'status',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'platform' => PlatformEnum::class,
|
'platform' => PlatformEnum::class,
|
||||||
'settings' => 'array',
|
'settings' => 'array',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'last_tested_at' => 'datetime'
|
'last_tested_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Encrypt password when storing
|
// Encrypt password when storing
|
||||||
|
|
@ -93,7 +94,6 @@ protected function password(): Attribute
|
||||||
)->withoutObjectCaching();
|
)->withoutObjectCaching();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Get the active accounts for a platform (returns collection)
|
// Get the active accounts for a platform (returns collection)
|
||||||
/**
|
/**
|
||||||
* @return Collection<int, PlatformAccount>
|
* @return Collection<int, PlatformAccount>
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,16 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static findMany(mixed $channel_ids)
|
* @method static findMany(mixed $channel_ids)
|
||||||
* @method static create(array $array)
|
* @method static create(array<string, mixed> $array)
|
||||||
* @property integer $id
|
*
|
||||||
* @property integer $platform_instance_id
|
* @property int $id
|
||||||
|
* @property int $platform_instance_id
|
||||||
* @property PlatformInstance $platformInstance
|
* @property PlatformInstance $platformInstance
|
||||||
* @property integer $channel_id
|
* @property int $channel_id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property int $language_id
|
* @property int $language_id
|
||||||
* @property Language|null $language
|
* @property Language|null $language
|
||||||
* @property boolean $is_active
|
* @property bool $is_active
|
||||||
*/
|
*/
|
||||||
class PlatformChannel extends Model
|
class PlatformChannel extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -34,11 +35,11 @@ class PlatformChannel extends Model
|
||||||
'channel_id',
|
'channel_id',
|
||||||
'description',
|
'description',
|
||||||
'language_id',
|
'language_id',
|
||||||
'is_active'
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -74,7 +75,7 @@ public function getFullNameAttribute(): string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<Feed, $this, Route>
|
* @return BelongsToMany<Feed, $this>
|
||||||
*/
|
*/
|
||||||
public function feeds(): BelongsToMany
|
public function feeds(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|
@ -84,7 +85,7 @@ public function feeds(): BelongsToMany
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<Feed, $this, Route>
|
* @return BelongsToMany<Feed, $this>
|
||||||
*/
|
*/
|
||||||
public function activeFeeds(): BelongsToMany
|
public function activeFeeds(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
*/
|
*/
|
||||||
class PlatformChannelPost extends Model
|
class PlatformChannelPost extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<\Illuminate\Database\Eloquent\Factories\Factory<PlatformChannelPost>> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'platform',
|
'platform',
|
||||||
'channel_id',
|
'channel_id',
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@
|
||||||
/**
|
/**
|
||||||
* @method static updateOrCreate(array<string, mixed> $array, $instanceData)
|
* @method static updateOrCreate(array<string, mixed> $array, $instanceData)
|
||||||
* @method static where(string $string, mixed $operator)
|
* @method static where(string $string, mixed $operator)
|
||||||
|
*
|
||||||
* @property PlatformEnum $platform
|
* @property PlatformEnum $platform
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $description
|
* @property string $description
|
||||||
* @property boolean $is_active
|
* @property bool $is_active
|
||||||
*/
|
*/
|
||||||
class PlatformInstance extends Model
|
class PlatformInstance extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -28,12 +29,12 @@ class PlatformInstance extends Model
|
||||||
'url',
|
'url',
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'is_active'
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'platform' => PlatformEnum::class,
|
'platform' => PlatformEnum::class,
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
use Database\Factories\RouteFactory;
|
use Database\Factories\RouteFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,17 +26,18 @@ class Route extends Model
|
||||||
|
|
||||||
// Laravel doesn't handle composite primary keys well, so we'll use regular queries
|
// Laravel doesn't handle composite primary keys well, so we'll use regular queries
|
||||||
protected $primaryKey = null;
|
protected $primaryKey = null;
|
||||||
|
|
||||||
public $incrementing = false;
|
public $incrementing = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
'is_active',
|
'is_active',
|
||||||
'priority'
|
'priority',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,18 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\SettingFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static updateOrCreate(string[] $array, array $array1)
|
* @method static updateOrCreate(array<string, string> $array, array<string, mixed> $array1)
|
||||||
* @method static create(string[] $array)
|
* @method static create(array<string, string> $array)
|
||||||
* @method static where(string $string, string $key)
|
* @method static where(string $string, string $key)
|
||||||
*/
|
*/
|
||||||
class Setting extends Model
|
class Setting extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<SettingFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = ['key', 'value'];
|
protected $fillable = ['key', 'value'];
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, HasApiTokens;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
namespace App\Modules\Lemmy;
|
namespace App\Modules\Lemmy;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Http\Client\Response;
|
use Illuminate\Http\Client\Response;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class LemmyRequest
|
class LemmyRequest
|
||||||
{
|
{
|
||||||
private string $instance;
|
private string $instance;
|
||||||
|
|
||||||
private ?string $token;
|
private ?string $token;
|
||||||
|
|
||||||
private string $scheme = 'https';
|
private string $scheme = 'https';
|
||||||
|
|
||||||
public function __construct(string $instance, ?string $token = null)
|
public function __construct(string $instance, ?string $token = null)
|
||||||
|
|
@ -45,6 +47,7 @@ public function withScheme(string $scheme): self
|
||||||
if (in_array($scheme, ['http', 'https'], true)) {
|
if (in_array($scheme, ['http', 'https'], true)) {
|
||||||
$this->scheme = $scheme;
|
$this->scheme = $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +86,7 @@ public function post(string $endpoint, array $data = []): Response
|
||||||
public function withToken(string $token): self
|
public function withToken(string $token): self
|
||||||
{
|
{
|
||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ public function login(string $username, string $password): ?string
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $response->json();
|
$data = $response->json();
|
||||||
|
|
||||||
return $data['jwt'] ?? null;
|
return $data['jwt'] ?? null;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Re-throw rate limit exceptions immediately
|
// Re-throw rate limit exceptions immediately
|
||||||
|
|
@ -93,6 +94,7 @@ public function getCommunityId(string $communityName, string $token): int
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $response->json();
|
$data = $response->json();
|
||||||
|
|
||||||
return $data['community_view']['community']['id'] ?? throw new Exception('Community not found');
|
return $data['community_view']['community']['id'] ?? throw new Exception('Community not found');
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Community lookup failed', ['error' => $e->getMessage()]);
|
logger()->error('Community lookup failed', ['error' => $e->getMessage()]);
|
||||||
|
|
@ -107,14 +109,15 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
|
||||||
$response = $request->get('post/list', [
|
$response = $request->get('post/list', [
|
||||||
'community_id' => $platformChannelId,
|
'community_id' => $platformChannelId,
|
||||||
'limit' => 50,
|
'limit' => 50,
|
||||||
'sort' => 'New'
|
'sort' => 'New',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! $response->successful()) {
|
if (! $response->successful()) {
|
||||||
logger()->warning('Failed to sync channel posts', [
|
logger()->warning('Failed to sync channel posts', [
|
||||||
'status' => $response->status(),
|
'status' => $response->status(),
|
||||||
'platform_channel_id' => $platformChannelId
|
'platform_channel_id' => $platformChannelId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,13 +140,13 @@ public function syncChannelPosts(string $token, int $platformChannelId, string $
|
||||||
|
|
||||||
logger()->info('Synced channel posts', [
|
logger()->info('Synced channel posts', [
|
||||||
'platform_channel_id' => $platformChannelId,
|
'platform_channel_id' => $platformChannelId,
|
||||||
'posts_count' => count($posts)
|
'posts_count' => count($posts),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Exception while syncing channel posts', [
|
logger()->error('Exception while syncing channel posts', [
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'platform_channel_id' => $platformChannelId
|
'platform_channel_id' => $platformChannelId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,17 +201,20 @@ public function getLanguages(): array
|
||||||
|
|
||||||
if (! $response->successful()) {
|
if (! $response->successful()) {
|
||||||
logger()->warning('Failed to fetch site languages', [
|
logger()->warning('Failed to fetch site languages', [
|
||||||
'status' => $response->status()
|
'status' => $response->status(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $response->json();
|
$data = $response->json();
|
||||||
|
|
||||||
return $data['all_languages'] ?? [];
|
return $data['all_languages'] ?? [];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Exception while fetching languages', [
|
logger()->error('Exception while fetching languages', [
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
class LemmyPublisher
|
class LemmyPublisher
|
||||||
{
|
{
|
||||||
private LemmyApiService $api;
|
private LemmyApiService $api;
|
||||||
|
|
||||||
private PlatformAccount $account;
|
private PlatformAccount $account;
|
||||||
|
|
||||||
public function __construct(PlatformAccount $account)
|
public function __construct(PlatformAccount $account)
|
||||||
|
|
@ -23,6 +24,7 @@ public function __construct(PlatformAccount $account)
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $extractedData
|
* @param array<string, mixed> $extractedData
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
|
*
|
||||||
* @throws PlatformAuthException
|
* @throws PlatformAuthException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
|
@ -37,6 +39,7 @@ public function publishToChannel(Article $article, array $extractedData, Platfor
|
||||||
// If the cached token was stale, refresh and retry once
|
// If the cached token was stale, refresh and retry once
|
||||||
if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
|
if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
|
||||||
$token = $authService->refreshToken($this->account);
|
$token = $authService->refreshToken($this->account);
|
||||||
|
|
||||||
return $this->createPost($token, $extractedData, $channel, $article);
|
return $this->createPost($token, $extractedData, $channel, $article);
|
||||||
}
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|
@ -65,5 +68,4 @@ private function createPost(string $token, array $extractedData, PlatformChannel
|
||||||
$languageId
|
$languageId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ExceptionOccurred;
|
use App\Events\ExceptionOccurred;
|
||||||
|
use App\Listeners\LogActionListener;
|
||||||
use App\Listeners\LogExceptionToDatabase;
|
use App\Listeners\LogExceptionToDatabase;
|
||||||
use Error;
|
use Error;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
|
|
@ -14,12 +16,15 @@
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
public function register(): void
|
public function register(): void {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
Event::listen(
|
||||||
|
ActionPerformed::class,
|
||||||
|
LogActionListener::class,
|
||||||
|
);
|
||||||
|
|
||||||
Event::listen(
|
Event::listen(
|
||||||
ExceptionOccurred::class,
|
ExceptionOccurred::class,
|
||||||
LogExceptionToDatabase::class,
|
LogExceptionToDatabase::class,
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Services\Http\HttpFetcher;
|
|
||||||
use App\Services\Factories\ArticleParserFactory;
|
use App\Services\Factories\ArticleParserFactory;
|
||||||
use App\Services\Factories\HomepageParserFactory;
|
use App\Services\Factories\HomepageParserFactory;
|
||||||
|
use App\Services\Http\HttpFetcher;
|
||||||
use App\Services\Log\LogSaver;
|
use App\Services\Log\LogSaver;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
@ -28,9 +28,9 @@ public function getArticlesFromFeed(Feed $feed): Collection
|
||||||
return $this->getArticlesFromWebsiteFeed($feed);
|
return $this->getArticlesFromWebsiteFeed($feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logSaver->warning("Unsupported feed type", null, [
|
$this->logSaver->warning('Unsupported feed type', null, [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'feed_type' => $feed->type
|
'feed_type' => $feed->type,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return collect();
|
return collect();
|
||||||
|
|
@ -54,7 +54,7 @@ private function getArticlesFromRssFeed(Feed $feed): Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($rss === false || ! isset($rss->channel->item)) {
|
if ($rss === false || ! isset($rss->channel->item)) {
|
||||||
$this->logSaver->warning("Failed to parse RSS feed XML", null, [
|
$this->logSaver->warning('Failed to parse RSS feed XML', null, [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'feed_url' => $feed->url,
|
'feed_url' => $feed->url,
|
||||||
]);
|
]);
|
||||||
|
|
@ -72,7 +72,7 @@ private function getArticlesFromRssFeed(Feed $feed): Collection
|
||||||
|
|
||||||
return $articles;
|
return $articles;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logSaver->error("Failed to fetch articles from RSS feed", null, [
|
$this->logSaver->error('Failed to fetch articles from RSS feed', null, [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'feed_url' => $feed->url,
|
'feed_url' => $feed->url,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
|
|
@ -92,9 +92,9 @@ private function getArticlesFromWebsiteFeed(Feed $feed): Collection
|
||||||
$parser = HomepageParserFactory::getParserForFeed($feed);
|
$parser = HomepageParserFactory::getParserForFeed($feed);
|
||||||
|
|
||||||
if (! $parser) {
|
if (! $parser) {
|
||||||
$this->logSaver->warning("No parser available for feed URL", null, [
|
$this->logSaver->warning('No parser available for feed URL', null, [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'feed_url' => $feed->url
|
'feed_url' => $feed->url,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return collect();
|
return collect();
|
||||||
|
|
@ -107,10 +107,10 @@ private function getArticlesFromWebsiteFeed(Feed $feed): Collection
|
||||||
->map(fn (string $url) => $this->saveArticle($url, $feed->id));
|
->map(fn (string $url) => $this->saveArticle($url, $feed->id));
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logSaver->error("Failed to fetch articles from website feed", null, [
|
$this->logSaver->error('Failed to fetch articles from website feed', null, [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'feed_url' => $feed->url,
|
'feed_url' => $feed->url,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return collect();
|
return collect();
|
||||||
|
|
@ -130,7 +130,7 @@ public function fetchArticleData(Article $article): array
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logSaver->error('Exception while fetching article data', null, [
|
$this->logSaver->error('Exception while fetching article data', null, [
|
||||||
'url' => $article->url,
|
'url' => $article->url,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -156,7 +156,7 @@ private function saveArticle(string $url, ?int $feedId = null): Article
|
||||||
|
|
||||||
return $article;
|
return $article;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logSaver->error("Failed to create article", null, [
|
$this->logSaver->error('Failed to create article', null, [
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'feed_id' => $feedId,
|
'feed_id' => $feedId,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ public function validate(Article $article): Article
|
||||||
if (! isset($articleData['full_article']) || empty($articleData['full_article'])) {
|
if (! isset($articleData['full_article']) || empty($articleData['full_article'])) {
|
||||||
logger()->warning('Article data missing full_article content', [
|
logger()->warning('Article data missing full_article content', [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'url' => $article->url
|
'url' => $article->url,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$updateData['approval_status'] = 'rejected';
|
$updateData['approval_status'] = 'rejected';
|
||||||
|
|
@ -67,7 +67,7 @@ private function validateByKeywords(string $full_article): bool
|
||||||
|
|
||||||
// Common Belgian news topics
|
// Common Belgian news topics
|
||||||
'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy',
|
'economy', 'economic', 'education', 'healthcare', 'transport', 'climate', 'energy',
|
||||||
'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police'
|
'European', 'EU', 'migration', 'security', 'justice', 'culture', 'police',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($keywords as $keyword) {
|
foreach ($keywords as $keyword) {
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,12 @@ public function refreshToken(PlatformAccount $account): string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate with Lemmy API and return user data with JWT
|
* Authenticate with Lemmy API and return user data with JWT
|
||||||
|
*
|
||||||
* @throws PlatformAuthException
|
* @throws PlatformAuthException
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
public function authenticate(string $instanceUrl, string $username, string $password): array
|
public function authenticate(string $instanceUrl, string $username, string $password): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -75,8 +79,8 @@ public function authenticate(string $instanceUrl, string $username, string $pass
|
||||||
'id' => 0, // Would need API call to get actual user info
|
'id' => 0, // Would need API call to get actual user info
|
||||||
'display_name' => null,
|
'display_name' => null,
|
||||||
'bio' => null,
|
'bio' => null,
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
} catch (PlatformAuthException $e) {
|
} catch (PlatformAuthException $e) {
|
||||||
// Re-throw PlatformAuthExceptions as-is to avoid nesting
|
// Re-throw PlatformAuthExceptions as-is to avoid nesting
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class DashboardStatsService
|
class DashboardStatsService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
public function getStats(string $period = 'today'): array
|
public function getStats(string $period = 'today'): array
|
||||||
{
|
{
|
||||||
$dateRange = $this->getDateRange($period);
|
$dateRange = $this->getDateRange($period);
|
||||||
|
|
@ -73,6 +75,9 @@ private function getDateRange(string $period): ?array
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
public function getSystemStats(): array
|
public function getSystemStats(): array
|
||||||
{
|
{
|
||||||
$totalFeeds = Feed::query()->count();
|
$totalFeeds = Feed::query()->count();
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
use App\Contracts\ArticleParserInterface;
|
use App\Contracts\ArticleParserInterface;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Services\Parsers\VrtArticleParser;
|
|
||||||
use App\Services\Parsers\BelgaArticleParser;
|
use App\Services\Parsers\BelgaArticleParser;
|
||||||
use App\Services\Parsers\GuardianArticleParser;
|
use App\Services\Parsers\GuardianArticleParser;
|
||||||
|
use App\Services\Parsers\VrtArticleParser;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class ArticleParserFactory
|
class ArticleParserFactory
|
||||||
|
|
@ -26,7 +26,7 @@ class ArticleParserFactory
|
||||||
public static function getParser(string $url): ArticleParserInterface
|
public static function getParser(string $url): ArticleParserInterface
|
||||||
{
|
{
|
||||||
foreach (self::$parsers as $parserClass) {
|
foreach (self::$parsers as $parserClass) {
|
||||||
$parser = new $parserClass();
|
$parser = new $parserClass;
|
||||||
|
|
||||||
if ($parser->canParse($url)) {
|
if ($parser->canParse($url)) {
|
||||||
return $parser;
|
return $parser;
|
||||||
|
|
@ -47,12 +47,13 @@ public static function getParserForFeed(Feed $feed, string $parserType = 'articl
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var class-string<ArticleParserInterface> $parserClass */
|
||||||
$parserClass = $providerConfig['parsers'][$parserType];
|
$parserClass = $providerConfig['parsers'][$parserType];
|
||||||
if (! class_exists($parserClass)) {
|
if (! class_exists($parserClass)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new $parserClass();
|
return new $parserClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,7 +62,8 @@ public static function getParserForFeed(Feed $feed, string $parserType = 'articl
|
||||||
public static function getSupportedSources(): array
|
public static function getSupportedSources(): array
|
||||||
{
|
{
|
||||||
return array_map(function ($parserClass) {
|
return array_map(function ($parserClass) {
|
||||||
$parser = new $parserClass();
|
$parser = new $parserClass;
|
||||||
|
|
||||||
return $parser->getSourceName();
|
return $parser->getSourceName();
|
||||||
}, self::$parsers);
|
}, self::$parsers);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,13 @@ public static function getParserForFeed(Feed $feed): ?HomepageParserInterface
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var class-string<HomepageParserInterface> $parserClass */
|
||||||
$parserClass = $providerConfig['parsers']['homepage'];
|
$parserClass = $providerConfig['parsers']['homepage'];
|
||||||
if (! class_exists($parserClass)) {
|
if (! class_exists($parserClass)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$language = $feed->language?->short_code ?? 'en';
|
$language = $feed->language->short_code ?? 'en';
|
||||||
|
|
||||||
return new $parserClass($language);
|
return new $parserClass($language);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
namespace App\Services\Http;
|
namespace App\Services\Http;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class HttpFetcher
|
class HttpFetcher
|
||||||
{
|
{
|
||||||
|
|
@ -23,7 +23,7 @@ public static function fetchHtml(string $url): string
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('HTTP fetch failed', [
|
logger()->error('HTTP fetch failed', [
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|
@ -48,20 +48,20 @@ public static function fetchMultipleUrls(array $urls): array
|
||||||
->reject(fn ($response, $index) => $response instanceof Exception)
|
->reject(fn ($response, $index) => $response instanceof Exception)
|
||||||
->map(function ($response, $index) use ($urls) {
|
->map(function ($response, $index) use ($urls) {
|
||||||
$url = $urls[$index];
|
$url = $urls[$index];
|
||||||
|
/** @var \Illuminate\Http\Client\Response $response */
|
||||||
try {
|
try {
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
return [
|
return [
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'html' => $response->body(),
|
'html' => $response->body(),
|
||||||
'success' => true
|
'success' => true,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'html' => null,
|
'html' => null,
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'status' => $response->status()
|
'status' => $response->status(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
|
|
@ -69,11 +69,10 @@ public static function fetchMultipleUrls(array $urls): array
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'html' => null,
|
'html' => null,
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'Exception occurred'
|
'error' => 'Exception occurred',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
->filter(fn($result) => $result !== null)
|
|
||||||
->toArray();
|
->toArray();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]);
|
logger()->error('Multiple URL fetch failed', ['error' => $e->getMessage()]);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ public function debug(string $message, ?PlatformChannel $channel = null, array $
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $context
|
* @param array<string, mixed> $context
|
||||||
*/
|
*/
|
||||||
private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
|
public function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
|
||||||
{
|
{
|
||||||
$logContext = $context;
|
$logContext = $context;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,15 +12,13 @@
|
||||||
use App\Modules\Lemmy\Services\LemmyPublisher;
|
use App\Modules\Lemmy\Services\LemmyPublisher;
|
||||||
use App\Services\Log\LogSaver;
|
use App\Services\Log\LogSaver;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class ArticlePublishingService
|
class ArticlePublishingService
|
||||||
{
|
{
|
||||||
public function __construct(private LogSaver $logSaver)
|
public function __construct(private LogSaver $logSaver) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Factory seam to create publisher instances (helps testing without network calls)
|
* Factory seam to create publisher instances (helps testing without network calls)
|
||||||
*/
|
*/
|
||||||
|
|
@ -28,9 +26,11 @@ protected function makePublisher(mixed $account): LemmyPublisher
|
||||||
{
|
{
|
||||||
return new LemmyPublisher($account);
|
return new LemmyPublisher($account);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $extractedData
|
* @param array<string, mixed> $extractedData
|
||||||
* @return Collection<int, ArticlePublication>
|
* @return Collection<int, ArticlePublication>
|
||||||
|
*
|
||||||
* @throws PublishException
|
* @throws PublishException
|
||||||
*/
|
*/
|
||||||
public function publishToRoutedChannels(Article $article, array $extractedData): Collection
|
public function publishToRoutedChannels(Article $article, array $extractedData): Collection
|
||||||
|
|
@ -60,7 +60,7 @@ public function publishToRoutedChannels(Article $article, array $extractedData):
|
||||||
if (! $account) {
|
if (! $account) {
|
||||||
$this->logSaver->warning('No active account for channel', $channel, [
|
$this->logSaver->warning('No active account for channel', $channel, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'route_priority' => $route->priority
|
'route_priority' => $route->priority,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -73,6 +73,7 @@ public function publishToRoutedChannels(Article $article, array $extractedData):
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a route matches an article based on keywords
|
* Check if a route matches an article based on keywords
|
||||||
|
*
|
||||||
* @param array<string, mixed> $extractedData
|
* @param array<string, mixed> $extractedData
|
||||||
*/
|
*/
|
||||||
private function routeMatchesArticle(Route $route, array $extractedData): bool
|
private function routeMatchesArticle(Route $route, array $extractedData): bool
|
||||||
|
|
@ -145,14 +146,14 @@ private function publishToChannel(Article $article, array $extractedData, Platfo
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->logSaver->info('Published to channel via keyword-filtered routing', $channel, [
|
$this->logSaver->info('Published to channel via keyword-filtered routing', $channel, [
|
||||||
'article_id' => $article->id
|
'article_id' => $article->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $publication;
|
return $publication;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logSaver->warning('Failed to publish to channel', $channel, [
|
$this->logSaver->warning('Failed to publish to channel', $channel, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class RoutingValidationService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, PlatformChannel> $channels
|
* @param Collection<int, PlatformChannel> $channels
|
||||||
|
*
|
||||||
* @throws RoutingMismatchException
|
* @throws RoutingMismatchException
|
||||||
*/
|
*/
|
||||||
public function validateLanguageCompatibility(Feed $feed, Collection $channels): void
|
public function validateLanguageCompatibility(Feed $feed, Collection $channels): void
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Route;
|
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
|
||||||
class SystemStatusService
|
class SystemStatusService
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"phpstan/phpstan": "^2.1",
|
"phpstan/phpstan": "^2.1",
|
||||||
|
"phpstan/phpstan-mockery": "^2.0",
|
||||||
"phpunit/phpunit": "^11.5.3"
|
"phpunit/phpunit": "^11.5.3"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,12 @@
|
||||||
'code' => 'belga',
|
'code' => 'belga',
|
||||||
'name' => 'Belga News Agency',
|
'name' => 'Belga News Agency',
|
||||||
'description' => 'Belgian national news agency',
|
'description' => 'Belgian national news agency',
|
||||||
'type' => 'website',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'languages' => [
|
'languages' => [
|
||||||
'en' => ['url' => 'https://www.belganewsagency.eu/'],
|
'en' => ['url' => 'https://www.belganewsagency.eu/feed'],
|
||||||
],
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => \App\Services\Parsers\BelgaHomepageParserAdapter::class,
|
|
||||||
'article' => \App\Services\Parsers\BelgaArticleParser::class,
|
'article' => \App\Services\Parsers\BelgaArticleParser::class,
|
||||||
'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class,
|
'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\ArticlePublication;
|
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
|
use App\Models\ArticlePublication;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ public function belga(): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
'provider' => 'belga',
|
'provider' => 'belga',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
|
'type' => 'rss',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ public function inactive(): static
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function community(string $name = null): static
|
public function community(?string $name = null): static
|
||||||
{
|
{
|
||||||
$communityName = $name ?: $this->faker->word();
|
$communityName = $name ?: $this->faker->word();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\Route;
|
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
class RouteFactory extends Factory
|
class RouteFactory extends Factory
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ public function run(): void
|
||||||
'name' => 'Belgae Social',
|
'name' => 'Belgae Social',
|
||||||
'description' => 'A Belgian Lemmy instance on the fediverse',
|
'description' => 'A Belgian Lemmy instance on the fediverse',
|
||||||
],
|
],
|
||||||
])->each (fn ($instanceData) =>
|
])->each(fn ($instanceData) => PlatformInstance::updateOrCreate(
|
||||||
PlatformInstance::updateOrCreate(
|
|
||||||
[
|
[
|
||||||
'platform' => $instanceData['platform'],
|
'platform' => $instanceData['platform'],
|
||||||
'url' => $instanceData['url'],
|
'url' => $instanceData['url'],
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class SettingsSeeder extends Seeder
|
class SettingsSeeder extends Seeder
|
||||||
|
|
|
||||||
109
phpstan-baseline.neon
Normal file
109
phpstan-baseline.neon
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
includes:
|
includes:
|
||||||
- vendor/larastan/larastan/extension.neon
|
- vendor/larastan/larastan/extension.neon
|
||||||
|
- vendor/phpstan/phpstan-mockery/extension.neon
|
||||||
|
- phpstan-baseline.neon
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
level: 7
|
level: 7
|
||||||
|
|
@ -10,3 +12,7 @@ parameters:
|
||||||
excludePaths:
|
excludePaths:
|
||||||
- bootstrap/*.php
|
- bootstrap/*.php
|
||||||
- storage/*
|
- storage/*
|
||||||
|
|
||||||
|
ignoreErrors:
|
||||||
|
- identifier: method.alreadyNarrowedType
|
||||||
|
- identifier: function.alreadyNarrowedType
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>Feed: {{ $route->feed?->name }}</span>
|
<span>Feed: {{ $route->feed?->name }}</span>
|
||||||
<span>•</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>•</span>
|
||||||
<span>Created: {{ $route->created_at->format('M d, Y') }}</span>
|
<span>Created: {{ $route->created_at->format('M d, Y') }}</span>
|
||||||
</div>
|
</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 }}
|
<strong>Feed:</strong> {{ $editingRoute->feed?->name }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-600">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
use App\Http\Controllers\Api\V1\AuthController;
|
use App\Http\Controllers\Api\V1\AuthController;
|
||||||
use App\Http\Controllers\Api\V1\DashboardController;
|
use App\Http\Controllers\Api\V1\DashboardController;
|
||||||
use App\Http\Controllers\Api\V1\FeedsController;
|
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\LogsController;
|
||||||
use App\Http\Controllers\Api\V1\OnboardingController;
|
use App\Http\Controllers\Api\V1\OnboardingController;
|
||||||
use App\Http\Controllers\Api\V1\PlatformAccountsController;
|
use App\Http\Controllers\Api\V1\PlatformAccountsController;
|
||||||
use App\Http\Controllers\Api\V1\PlatformChannelsController;
|
use App\Http\Controllers\Api\V1\PlatformChannelsController;
|
||||||
use App\Http\Controllers\Api\V1\RoutingController;
|
use App\Http\Controllers\Api\V1\RoutingController;
|
||||||
use App\Http\Controllers\Api\V1\KeywordsController;
|
|
||||||
use App\Http\Controllers\Api\V1\SettingsController;
|
use App\Http\Controllers\Api\V1\SettingsController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\Article;
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -31,7 +26,7 @@ public function test_api_routes_are_publicly_accessible(): void
|
||||||
'/api/v1/feeds',
|
'/api/v1/feeds',
|
||||||
'/api/v1/routing',
|
'/api/v1/routing',
|
||||||
'/api/v1/settings',
|
'/api/v1/settings',
|
||||||
'/api/v1/logs'
|
'/api/v1/logs',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($routes as $route) {
|
foreach ($routes as $route) {
|
||||||
|
|
@ -49,7 +44,7 @@ public function test_fallback_route_returns_api_message(): void
|
||||||
$response->assertStatus(404);
|
$response->assertStatus(404);
|
||||||
$response->assertJson([
|
$response->assertJson([
|
||||||
'message' => 'This is the FFR API backend. Use /api/v1/* endpoints or check the React frontend.',
|
'message' => 'This is the FFR API backend. Use /api/v1/* endpoints or check the React frontend.',
|
||||||
'api_base' => '/api/v1'
|
'api_base' => '/api/v1',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,12 +27,12 @@ public function test_user_model_creates_successfully(): void
|
||||||
{
|
{
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'name' => 'Test User',
|
'name' => 'Test User',
|
||||||
'email' => 'test@example.com'
|
'email' => 'test@example.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'name' => 'Test User',
|
'name' => 'Test User',
|
||||||
'email' => 'test@example.com'
|
'email' => 'test@example.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('Test User', $user->name);
|
$this->assertEquals('Test User', $user->name);
|
||||||
|
|
@ -43,38 +43,40 @@ public function test_language_model_creates_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create([
|
$language = Language::factory()->create([
|
||||||
'name' => 'English',
|
'name' => 'English',
|
||||||
'short_code' => 'en'
|
'short_code' => 'en',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('languages', [
|
$this->assertDatabaseHas('languages', [
|
||||||
'name' => 'English',
|
'name' => 'English',
|
||||||
'short_code' => 'en'
|
'short_code' => 'en',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_platform_instance_model_creates_successfully(): void
|
public function test_platform_instance_model_creates_successfully(): void
|
||||||
{
|
{
|
||||||
|
/** @var PlatformInstance $instance */
|
||||||
$instance = PlatformInstance::factory()->create([
|
$instance = PlatformInstance::factory()->create([
|
||||||
'name' => 'Test Instance',
|
'name' => 'Test Instance',
|
||||||
'url' => 'https://test.lemmy.world'
|
'url' => 'https://test.lemmy.world',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_instances', [
|
$this->assertDatabaseHas('platform_instances', [
|
||||||
'name' => 'Test Instance',
|
'name' => 'Test Instance',
|
||||||
'url' => 'https://test.lemmy.world'
|
'url' => 'https://test.lemmy.world',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_platform_account_model_creates_successfully(): void
|
public function test_platform_account_model_creates_successfully(): void
|
||||||
{
|
{
|
||||||
|
/** @var PlatformAccount $account */
|
||||||
$account = PlatformAccount::factory()->create([
|
$account = PlatformAccount::factory()->create([
|
||||||
'username' => 'testuser',
|
'username' => 'testuser',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_accounts', [
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
'username' => 'testuser',
|
'username' => 'testuser',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('testuser', $account->username);
|
$this->assertEquals('testuser', $account->username);
|
||||||
|
|
@ -89,14 +91,14 @@ public function test_platform_channel_model_creates_successfully(): void
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
'name' => 'Test Channel',
|
'name' => 'Test Channel',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
'name' => 'Test Channel',
|
'name' => 'Test Channel',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($instance->id, $channel->platformInstance->id);
|
$this->assertEquals($instance->id, $channel->platformInstance->id);
|
||||||
|
|
@ -111,14 +113,14 @@ public function test_feed_model_creates_successfully(): void
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
'url' => 'https://example.com/feed.rss',
|
'url' => 'https://example.com/feed.rss',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
'url' => 'https://example.com/feed.rss',
|
'url' => 'https://example.com/feed.rss',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($language->id, $feed->language->id);
|
$this->assertEquals($language->id, $feed->language->id);
|
||||||
|
|
@ -132,14 +134,14 @@ public function test_article_model_creates_successfully(): void
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
'url' => 'https://example.com/article',
|
'url' => 'https://example.com/article',
|
||||||
'approval_status' => 'pending'
|
'approval_status' => 'pending',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('articles', [
|
$this->assertDatabaseHas('articles', [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
'url' => 'https://example.com/article',
|
'url' => 'https://example.com/article',
|
||||||
'approval_status' => 'pending'
|
'approval_status' => 'pending',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($feed->id, $article->feed->id);
|
$this->assertEquals($feed->id, $article->feed->id);
|
||||||
|
|
@ -155,14 +157,14 @@ public function test_article_publication_model_creates_successfully(): void
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'post_id' => 'test-post-123',
|
'post_id' => 'test-post-123',
|
||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
'published_by' => 'test-user'
|
'published_by' => 'test-user',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('article_publications', [
|
$this->assertDatabaseHas('article_publications', [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'post_id' => 'test-post-123',
|
'post_id' => 'test-post-123',
|
||||||
'published_by' => 'test-user'
|
'published_by' => 'test-user',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($article->id, $publication->article->id);
|
$this->assertEquals($article->id, $publication->article->id);
|
||||||
|
|
@ -174,16 +176,17 @@ public function test_route_model_creates_successfully(): void
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create();
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
/** @var Route $route */
|
||||||
$route = Route::factory()->create([
|
$route = Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('routes', [
|
$this->assertDatabaseHas('routes', [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($feed->id, $route->feed->id);
|
$this->assertEquals($feed->id, $route->feed->id);
|
||||||
|
|
@ -228,14 +231,14 @@ public function test_keyword_model_creates_successfully(): void
|
||||||
->forChannel($channel)
|
->forChannel($channel)
|
||||||
->create([
|
->create([
|
||||||
'keyword' => 'test keyword',
|
'keyword' => 'test keyword',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('keywords', [
|
$this->assertDatabaseHas('keywords', [
|
||||||
'keyword' => 'test keyword',
|
'keyword' => 'test keyword',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id
|
'platform_channel_id' => $channel->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,12 +248,12 @@ public function test_log_model_creates_successfully(): void
|
||||||
'level' => 'info',
|
'level' => 'info',
|
||||||
'message' => 'Test log message',
|
'message' => 'Test log message',
|
||||||
'context' => json_encode(['key' => 'value']),
|
'context' => json_encode(['key' => 'value']),
|
||||||
'logged_at' => now()
|
'logged_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('logs', [
|
$this->assertDatabaseHas('logs', [
|
||||||
'level' => 'info',
|
'level' => 'info',
|
||||||
'message' => 'Test log message'
|
'message' => 'Test log message',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,12 +261,12 @@ public function test_setting_model_creates_successfully(): void
|
||||||
{
|
{
|
||||||
$setting = Setting::create([
|
$setting = Setting::create([
|
||||||
'key' => 'test_setting',
|
'key' => 'test_setting',
|
||||||
'value' => 'test_value'
|
'value' => 'test_value',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('settings', [
|
$this->assertDatabaseHas('settings', [
|
||||||
'key' => 'test_setting',
|
'key' => 'test_setting',
|
||||||
'value' => 'test_value'
|
'value' => 'test_value',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,7 +293,7 @@ public function test_platform_account_channels_many_to_many_relationship(): void
|
||||||
$this->assertDatabaseHas('platform_account_channels', [
|
$this->assertDatabaseHas('platform_account_channels', [
|
||||||
'platform_account_id' => $account->id,
|
'platform_account_id' => $account->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ public function test_command_skips_when_article_processing_disabled(): void
|
||||||
Queue::fake();
|
Queue::fake();
|
||||||
Setting::create([
|
Setting::create([
|
||||||
'key' => 'article_processing_enabled',
|
'key' => 'article_processing_enabled',
|
||||||
'value' => '0'
|
'value' => '0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
namespace Tests\Feature\Http\Console\Commands;
|
namespace Tests\Feature\Http\Console\Commands;
|
||||||
|
|
||||||
use App\Jobs\SyncChannelPostsJob;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Queue;
|
|
||||||
use Illuminate\Testing\PendingCommand;
|
use Illuminate\Testing\PendingCommand;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -36,6 +34,7 @@ public function test_command_returns_failure_exit_code_for_unsupported_platform(
|
||||||
public function test_command_accepts_lemmy_platform_argument(): void
|
public function test_command_accepts_lemmy_platform_argument(): void
|
||||||
{
|
{
|
||||||
// Act - Test that the command accepts lemmy as a valid platform argument
|
// Act - Test that the command accepts lemmy as a valid platform argument
|
||||||
|
/** @var PendingCommand $exitCode */
|
||||||
$exitCode = $this->artisan('channel:sync lemmy');
|
$exitCode = $this->artisan('channel:sync lemmy');
|
||||||
|
|
||||||
// Assert - Command should succeed (not fail with argument validation error)
|
// Assert - Command should succeed (not fail with argument validation error)
|
||||||
|
|
@ -46,6 +45,7 @@ public function test_command_accepts_lemmy_platform_argument(): void
|
||||||
public function test_command_handles_default_platform(): void
|
public function test_command_handles_default_platform(): void
|
||||||
{
|
{
|
||||||
// Act - Test that the command works with default platform (should be lemmy)
|
// Act - Test that the command works with default platform (should be lemmy)
|
||||||
|
/** @var PendingCommand $exitCode */
|
||||||
$exitCode = $this->artisan('channel:sync');
|
$exitCode = $this->artisan('channel:sync');
|
||||||
|
|
||||||
// Assert - Command should succeed with default platform
|
// Assert - Command should succeed with default platform
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -33,7 +32,7 @@ public function test_index_returns_successful_response(): void
|
||||||
'publishing_approvals_enabled',
|
'publishing_approvals_enabled',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'message'
|
'message',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +52,7 @@ public function test_index_returns_articles_with_pagination(): void
|
||||||
'total' => 25,
|
'total' => 25,
|
||||||
'last_page' => 3,
|
'last_page' => 3,
|
||||||
],
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertCount(10, $response->json('data.articles'));
|
$this->assertCount(10, $response->json('data.articles'));
|
||||||
|
|
@ -74,7 +73,7 @@ public function test_index_respects_per_page_limit(): void
|
||||||
'pagination' => [
|
'pagination' => [
|
||||||
'per_page' => 100, // Should be capped at 100
|
'per_page' => 100, // Should be capped at 100
|
||||||
],
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,13 +84,13 @@ public function test_index_orders_articles_by_created_at_desc(): void
|
||||||
$firstArticle = Article::factory()->create([
|
$firstArticle = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'created_at' => now()->subHours(2),
|
'created_at' => now()->subHours(2),
|
||||||
'title' => 'First Article'
|
'title' => 'First Article',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$secondArticle = Article::factory()->create([
|
$secondArticle = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'created_at' => now()->subHour(),
|
'created_at' => now()->subHour(),
|
||||||
'title' => 'Second Article'
|
'title' => 'Second Article',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->getJson('/api/v1/articles');
|
$response = $this->getJson('/api/v1/articles');
|
||||||
|
|
@ -108,7 +107,7 @@ public function test_approve_article_successfully(): void
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'pending'
|
'approval_status' => 'pending',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/articles/{$article->id}/approve");
|
$response = $this->postJson("/api/v1/articles/{$article->id}/approve");
|
||||||
|
|
@ -116,7 +115,7 @@ public function test_approve_article_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Article approved and queued for publishing.'
|
'message' => 'Article approved and queued for publishing.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$article->refresh();
|
$article->refresh();
|
||||||
|
|
@ -135,7 +134,7 @@ public function test_reject_article_successfully(): void
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'pending'
|
'approval_status' => 'pending',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/articles/{$article->id}/reject");
|
$response = $this->postJson("/api/v1/articles/{$article->id}/reject");
|
||||||
|
|
@ -143,7 +142,7 @@ public function test_reject_article_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Article rejected.'
|
'message' => 'Article rejected.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$article->refresh();
|
$article->refresh();
|
||||||
|
|
@ -165,9 +164,9 @@ public function test_index_includes_settings(): void
|
||||||
->assertJsonStructure([
|
->assertJsonStructure([
|
||||||
'data' => [
|
'data' => [
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'publishing_approvals_enabled'
|
'publishing_approvals_enabled',
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ public function test_stats_returns_successful_response(): void
|
||||||
'available_periods',
|
'available_periods',
|
||||||
'current_period',
|
'current_period',
|
||||||
],
|
],
|
||||||
'message'
|
'message',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ public function test_stats_with_different_periods(): void
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'current_period' => $period,
|
'current_period' => $period,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ public function test_stats_with_sample_data(): void
|
||||||
ArticlePublication::factory()->create([
|
ArticlePublication::factory()->create([
|
||||||
'article_id' => $articles->first()->id,
|
'article_id' => $articles->first()->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'published_at' => now()
|
'published_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->getJson('/api/v1/dashboard/stats?period=all');
|
$response = $this->getJson('/api/v1/dashboard/stats?period=all');
|
||||||
|
|
@ -93,7 +93,7 @@ public function test_stats_with_sample_data(): void
|
||||||
'articles_fetched' => $initialArticles + 3,
|
'articles_fetched' => $initialArticles + 3,
|
||||||
'articles_published' => $initialPublications + 1,
|
'articles_published' => $initialPublications + 1,
|
||||||
],
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Just verify structure and that we have more items than we started with
|
// Just verify structure and that we have more items than we started with
|
||||||
|
|
@ -126,7 +126,7 @@ public function test_stats_returns_empty_data_with_no_records(): void
|
||||||
'total_routes' => 0,
|
'total_routes' => 0,
|
||||||
'active_routes' => 0,
|
'active_routes' => 0,
|
||||||
],
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ public function test_index_returns_successful_response(): void
|
||||||
'success',
|
'success',
|
||||||
'data' => [
|
'data' => [
|
||||||
'feeds',
|
'feeds',
|
||||||
'pagination'
|
'pagination',
|
||||||
],
|
],
|
||||||
'message'
|
'message',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ public function test_store_creates_vrt_feed_successfully(): void
|
||||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
|
@ -98,16 +98,16 @@ public function test_store_creates_belga_feed_successfully(): void
|
||||||
'message' => 'Feed created successfully!',
|
'message' => 'Feed created successfully!',
|
||||||
'data' => [
|
'data' => [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
'type' => 'website',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
'type' => 'website',
|
'type' => 'rss',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +133,7 @@ public function test_store_creates_guardian_feed_successfully(): void
|
||||||
'url' => 'https://www.theguardian.com/international/rss',
|
'url' => 'https://www.theguardian.com/international/rss',
|
||||||
'type' => 'rss',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
|
@ -160,7 +160,7 @@ public function test_store_sets_default_active_status(): void
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'data' => [
|
'data' => [
|
||||||
'is_active' => true, // Should default to true
|
'is_active' => true, // Should default to true
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ public function test_show_returns_feed_successfully(): void
|
||||||
'data' => [
|
'data' => [
|
||||||
'id' => $feed->id,
|
'id' => $feed->id,
|
||||||
'name' => $feed->name,
|
'name' => $feed->name,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ public function test_update_modifies_feed_successfully(): void
|
||||||
'message' => 'Feed updated successfully!',
|
'message' => 'Feed updated successfully!',
|
||||||
'data' => [
|
'data' => [
|
||||||
'name' => 'Updated Name',
|
'name' => 'Updated Name',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
|
@ -260,7 +260,7 @@ public function test_update_preserves_active_status_when_not_provided(): void
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'data' => [
|
'data' => [
|
||||||
'is_active' => false, // Should preserve original value
|
'is_active' => false, // Should preserve original value
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,7 +298,7 @@ public function test_toggle_activates_inactive_feed(): void
|
||||||
'message' => 'Feed activated successfully!',
|
'message' => 'Feed activated successfully!',
|
||||||
'data' => [
|
'data' => [
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
|
@ -319,7 +319,7 @@ public function test_toggle_deactivates_active_feed(): void
|
||||||
'message' => 'Feed deactivated successfully!',
|
'message' => 'Feed deactivated successfully!',
|
||||||
'data' => [
|
'data' => [
|
||||||
'is_active' => false,
|
'is_active' => false,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ class KeywordsControllerTest extends TestCase
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
protected Feed $feed;
|
protected Feed $feed;
|
||||||
|
|
||||||
protected PlatformChannel $channel;
|
protected PlatformChannel $channel;
|
||||||
|
|
||||||
protected Route $route;
|
protected Route $route;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
|
|
@ -28,7 +30,7 @@ protected function setUp(): void
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id,
|
'platform_channel_id' => $this->channel->id,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'priority' => 50
|
'priority' => 50,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +40,7 @@ public function test_can_get_keywords_for_route(): void
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id,
|
'platform_channel_id' => $this->channel->id,
|
||||||
'keyword' => 'test keyword',
|
'keyword' => 'test keyword',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->getJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords");
|
$response = $this->getJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords");
|
||||||
|
|
@ -52,9 +54,9 @@ public function test_can_get_keywords_for_route(): void
|
||||||
'keyword',
|
'keyword',
|
||||||
'is_active',
|
'is_active',
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id'
|
'platform_channel_id',
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJsonPath('data.0.keyword', 'test keyword');
|
->assertJsonPath('data.0.keyword', 'test keyword');
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +65,7 @@ public function test_can_create_keyword_for_route(): void
|
||||||
{
|
{
|
||||||
$keywordData = [
|
$keywordData = [
|
||||||
'keyword' => 'new keyword',
|
'keyword' => 'new keyword',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", $keywordData);
|
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", $keywordData);
|
||||||
|
|
@ -76,8 +78,8 @@ public function test_can_create_keyword_for_route(): void
|
||||||
'keyword',
|
'keyword',
|
||||||
'is_active',
|
'is_active',
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id'
|
'platform_channel_id',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJsonPath('data.keyword', 'new keyword')
|
->assertJsonPath('data.keyword', 'new keyword')
|
||||||
->assertJsonPath('data.is_active', true);
|
->assertJsonPath('data.is_active', true);
|
||||||
|
|
@ -86,7 +88,7 @@ public function test_can_create_keyword_for_route(): void
|
||||||
'keyword' => 'new keyword',
|
'keyword' => 'new keyword',
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id,
|
'platform_channel_id' => $this->channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,11 +97,11 @@ public function test_cannot_create_duplicate_keyword_for_route(): void
|
||||||
Keyword::factory()->create([
|
Keyword::factory()->create([
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id,
|
'platform_channel_id' => $this->channel->id,
|
||||||
'keyword' => 'duplicate keyword'
|
'keyword' => 'duplicate keyword',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", [
|
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords", [
|
||||||
'keyword' => 'duplicate keyword'
|
'keyword' => 'duplicate keyword',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(409)
|
$response->assertStatus(409)
|
||||||
|
|
@ -109,14 +111,15 @@ public function test_cannot_create_duplicate_keyword_for_route(): void
|
||||||
|
|
||||||
public function test_can_update_keyword(): void
|
public function test_can_update_keyword(): void
|
||||||
{
|
{
|
||||||
|
/** @var Keyword $keyword */
|
||||||
$keyword = Keyword::factory()->create([
|
$keyword = Keyword::factory()->create([
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id,
|
'platform_channel_id' => $this->channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->putJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}", [
|
$response = $this->putJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}", [
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
|
|
@ -124,15 +127,16 @@ public function test_can_update_keyword(): void
|
||||||
|
|
||||||
$this->assertDatabaseHas('keywords', [
|
$this->assertDatabaseHas('keywords', [
|
||||||
'id' => $keyword->id,
|
'id' => $keyword->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_can_delete_keyword(): void
|
public function test_can_delete_keyword(): void
|
||||||
{
|
{
|
||||||
|
/** @var Keyword $keyword */
|
||||||
$keyword = Keyword::factory()->create([
|
$keyword = Keyword::factory()->create([
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id
|
'platform_channel_id' => $this->channel->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}");
|
$response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}");
|
||||||
|
|
@ -140,16 +144,17 @@ public function test_can_delete_keyword(): void
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('keywords', [
|
$this->assertDatabaseMissing('keywords', [
|
||||||
'id' => $keyword->id
|
'id' => $keyword->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_can_toggle_keyword(): void
|
public function test_can_toggle_keyword(): void
|
||||||
{
|
{
|
||||||
|
/** @var Keyword $keyword */
|
||||||
$keyword = Keyword::factory()->create([
|
$keyword = Keyword::factory()->create([
|
||||||
'feed_id' => $this->feed->id,
|
'feed_id' => $this->feed->id,
|
||||||
'platform_channel_id' => $this->channel->id,
|
'platform_channel_id' => $this->channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}/toggle");
|
$response = $this->postJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}/toggle");
|
||||||
|
|
@ -159,7 +164,7 @@ public function test_can_toggle_keyword(): void
|
||||||
|
|
||||||
$this->assertDatabaseHas('keywords', [
|
$this->assertDatabaseHas('keywords', [
|
||||||
'id' => $keyword->id,
|
'id' => $keyword->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,9 +173,10 @@ public function test_cannot_access_keyword_from_different_route(): void
|
||||||
$otherFeed = Feed::factory()->create();
|
$otherFeed = Feed::factory()->create();
|
||||||
$otherChannel = PlatformChannel::factory()->create();
|
$otherChannel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
/** @var Keyword $keyword */
|
||||||
$keyword = Keyword::factory()->create([
|
$keyword = Keyword::factory()->create([
|
||||||
'feed_id' => $otherFeed->id,
|
'feed_id' => $otherFeed->id,
|
||||||
'platform_channel_id' => $otherChannel->id
|
'platform_channel_id' => $otherChannel->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}");
|
$response = $this->deleteJson("/api/v1/routing/{$this->feed->id}/{$this->channel->id}/keywords/{$keyword->id}");
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public function test_index_returns_successful_response(): void
|
||||||
'context',
|
'context',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
'pagination' => [
|
'pagination' => [
|
||||||
'current_page',
|
'current_page',
|
||||||
|
|
@ -46,12 +46,12 @@ public function test_index_returns_successful_response(): void
|
||||||
'total',
|
'total',
|
||||||
'from',
|
'from',
|
||||||
'to',
|
'to',
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Logs retrieved successfully.'
|
'message' => 'Logs retrieved successfully.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,8 +147,8 @@ public function test_index_handles_empty_logs(): void
|
||||||
'total' => 0,
|
'total' => 0,
|
||||||
'current_page' => 1,
|
'current_page' => 1,
|
||||||
'last_page' => 1,
|
'last_page' => 1,
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ protected function setUp(): void
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_status_shows_needs_onboarding_when_no_components_exist()
|
public function test_status_shows_needs_onboarding_when_no_components_exist(): void
|
||||||
{
|
{
|
||||||
$response = $this->getJson('/api/v1/onboarding/status');
|
$response = $this->getJson('/api/v1/onboarding/status');
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ public function test_status_shows_needs_onboarding_when_no_components_exist()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_status_shows_feed_step_when_platform_account_exists()
|
public function test_status_shows_feed_step_when_platform_account_exists(): void
|
||||||
{
|
{
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ public function test_status_shows_feed_step_when_platform_account_exists()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_status_shows_channel_step_when_platform_account_and_feed_exist()
|
public function test_status_shows_channel_step_when_platform_account_and_feed_exist(): void
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
|
|
@ -91,7 +91,7 @@ public function test_status_shows_channel_step_when_platform_account_and_feed_ex
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_status_shows_route_step_when_platform_account_feed_and_channel_exist()
|
public function test_status_shows_route_step_when_platform_account_feed_and_channel_exist(): void
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
|
|
@ -114,7 +114,7 @@ public function test_status_shows_route_step_when_platform_account_feed_and_chan
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_status_shows_no_onboarding_needed_when_all_components_exist()
|
public function test_status_shows_no_onboarding_needed_when_all_components_exist(): void
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
|
|
@ -138,7 +138,7 @@ public function test_status_shows_no_onboarding_needed_when_all_components_exist
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_status_shows_no_onboarding_needed_when_skipped()
|
public function test_status_shows_no_onboarding_needed_when_skipped(): void
|
||||||
{
|
{
|
||||||
// No components exist but onboarding is skipped
|
// No components exist but onboarding is skipped
|
||||||
Setting::create([
|
Setting::create([
|
||||||
|
|
@ -163,7 +163,7 @@ public function test_status_shows_no_onboarding_needed_when_skipped()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_options_returns_languages_and_platform_instances()
|
public function test_options_returns_languages_and_platform_instances(): void
|
||||||
{
|
{
|
||||||
PlatformInstance::factory()->create([
|
PlatformInstance::factory()->create([
|
||||||
'platform' => 'lemmy',
|
'platform' => 'lemmy',
|
||||||
|
|
@ -179,34 +179,34 @@ public function test_options_returns_languages_and_platform_instances()
|
||||||
'success',
|
'success',
|
||||||
'data' => [
|
'data' => [
|
||||||
'languages' => [
|
'languages' => [
|
||||||
'*' => ['id', 'short_code', 'name', 'native_name', 'is_active']
|
'*' => ['id', 'short_code', 'name', 'native_name', 'is_active'],
|
||||||
],
|
],
|
||||||
'platform_instances' => [
|
'platform_instances' => [
|
||||||
'*' => ['id', 'platform', 'url', 'name', 'description', 'is_active']
|
'*' => ['id', 'platform', 'url', 'name', 'description', 'is_active'],
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_complete_onboarding_returns_success()
|
public function test_complete_onboarding_returns_success(): void
|
||||||
{
|
{
|
||||||
$response = $this->postJson('/api/v1/onboarding/complete');
|
$response = $this->postJson('/api/v1/onboarding/complete');
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => ['completed' => true]
|
'data' => ['completed' => true],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_skip_onboarding_creates_setting()
|
public function test_skip_onboarding_creates_setting(): void
|
||||||
{
|
{
|
||||||
$response = $this->postJson('/api/v1/onboarding/skip');
|
$response = $this->postJson('/api/v1/onboarding/skip');
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => ['skipped' => true]
|
'data' => ['skipped' => true],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('settings', [
|
$this->assertDatabaseHas('settings', [
|
||||||
|
|
@ -215,7 +215,7 @@ public function test_skip_onboarding_creates_setting()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_skip_onboarding_updates_existing_setting()
|
public function test_skip_onboarding_updates_existing_setting(): void
|
||||||
{
|
{
|
||||||
// Create existing setting with false value
|
// Create existing setting with false value
|
||||||
Setting::create([
|
Setting::create([
|
||||||
|
|
@ -236,7 +236,7 @@ public function test_skip_onboarding_updates_existing_setting()
|
||||||
$this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count());
|
$this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_reset_skip_removes_setting()
|
public function test_reset_skip_removes_setting(): void
|
||||||
{
|
{
|
||||||
// Create skipped setting
|
// Create skipped setting
|
||||||
Setting::create([
|
Setting::create([
|
||||||
|
|
@ -249,7 +249,7 @@ public function test_reset_skip_removes_setting()
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => ['reset' => true]
|
'data' => ['reset' => true],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('settings', [
|
$this->assertDatabaseMissing('settings', [
|
||||||
|
|
@ -257,18 +257,18 @@ public function test_reset_skip_removes_setting()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_reset_skip_works_when_no_setting_exists()
|
public function test_reset_skip_works_when_no_setting_exists(): void
|
||||||
{
|
{
|
||||||
$response = $this->postJson('/api/v1/onboarding/reset-skip');
|
$response = $this->postJson('/api/v1/onboarding/reset-skip');
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => ['reset' => true]
|
'data' => ['reset' => true],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_onboarding_flow_integration()
|
public function test_onboarding_flow_integration(): void
|
||||||
{
|
{
|
||||||
// 1. Initial status - needs onboarding
|
// 1. Initial status - needs onboarding
|
||||||
$response = $this->getJson('/api/v1/onboarding/status');
|
$response = $this->getJson('/api/v1/onboarding/status');
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,12 @@ public function test_index_returns_successful_response(): void
|
||||||
'is_active',
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform accounts retrieved successfully.'
|
'message' => 'Platform accounts retrieved successfully.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,11 +75,11 @@ public function test_store_creates_platform_account_successfully(): void
|
||||||
'is_active',
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform account created successfully!'
|
'message' => 'Platform account created successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_accounts', [
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
|
|
@ -115,7 +115,7 @@ public function test_show_returns_platform_account_successfully(): void
|
||||||
'is_active',
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|
@ -123,7 +123,7 @@ public function test_show_returns_platform_account_successfully(): void
|
||||||
'data' => [
|
'data' => [
|
||||||
'id' => $account->id,
|
'id' => $account->id,
|
||||||
'username' => $account->username,
|
'username' => $account->username,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ public function test_update_modifies_platform_account_successfully(): void
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'instance_url' => 'https://updated.example.com',
|
'instance_url' => 'https://updated.example.com',
|
||||||
'username' => 'updateduser',
|
'username' => 'updateduser',
|
||||||
'settings' => ['updated' => 'value']
|
'settings' => ['updated' => 'value'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->putJson("/api/v1/platform-accounts/{$account->id}", $updateData);
|
$response = $this->putJson("/api/v1/platform-accounts/{$account->id}", $updateData);
|
||||||
|
|
@ -142,7 +142,7 @@ public function test_update_modifies_platform_account_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform account updated successfully!'
|
'message' => 'Platform account updated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_accounts', [
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
|
|
@ -161,11 +161,11 @@ public function test_destroy_deletes_platform_account_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform account deleted successfully!'
|
'message' => 'Platform account deleted successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('platform_accounts', [
|
$this->assertDatabaseMissing('platform_accounts', [
|
||||||
'id' => $account->id
|
'id' => $account->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +182,7 @@ public function test_set_active_activates_platform_account(): void
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_accounts', [
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
'id' => $account->id,
|
'id' => $account->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,12 +190,12 @@ public function test_set_active_deactivates_other_accounts_of_same_platform(): v
|
||||||
{
|
{
|
||||||
$activeAccount = PlatformAccount::factory()->create([
|
$activeAccount = PlatformAccount::factory()->create([
|
||||||
'platform' => 'lemmy',
|
'platform' => 'lemmy',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$newAccount = PlatformAccount::factory()->create([
|
$newAccount = PlatformAccount::factory()->create([
|
||||||
'platform' => 'lemmy',
|
'platform' => 'lemmy',
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/platform-accounts/{$newAccount->id}/set-active");
|
$response = $this->postJson("/api/v1/platform-accounts/{$newAccount->id}/set-active");
|
||||||
|
|
@ -204,12 +204,12 @@ public function test_set_active_deactivates_other_accounts_of_same_platform(): v
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_accounts', [
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
'id' => $activeAccount->id,
|
'id' => $activeAccount->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_accounts', [
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
'id' => $newAccount->id,
|
'id' => $newAccount->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,13 +34,13 @@ public function test_index_returns_successful_response(): void
|
||||||
'is_active',
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
'platform_instance'
|
'platform_instance',
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform channels retrieved successfully.'
|
'message' => 'Platform channels retrieved successfully.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ public function test_store_creates_platform_channel_successfully(): void
|
||||||
// Create a platform account for this instance first
|
// Create a platform account for this instance first
|
||||||
PlatformAccount::factory()->create([
|
PlatformAccount::factory()->create([
|
||||||
'instance_url' => $instance->url,
|
'instance_url' => $instance->url,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
|
|
@ -76,11 +76,11 @@ public function test_store_creates_platform_channel_successfully(): void
|
||||||
'is_active',
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform channel created successfully and linked to platform account!'
|
'message' => 'Platform channel created successfully and linked to platform account!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
|
|
@ -102,7 +102,7 @@ public function test_store_validates_platform_instance_exists(): void
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
'platform_instance_id' => 999,
|
'platform_instance_id' => 999,
|
||||||
'name' => 'Test Channel'
|
'name' => 'Test Channel',
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/platform-channels', $data);
|
$response = $this->postJson('/api/v1/platform-channels', $data);
|
||||||
|
|
@ -132,8 +132,8 @@ public function test_show_returns_platform_channel_successfully(): void
|
||||||
'is_active',
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
'platform_instance'
|
'platform_instance',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|
@ -141,7 +141,7 @@ public function test_show_returns_platform_channel_successfully(): void
|
||||||
'data' => [
|
'data' => [
|
||||||
'id' => $channel->id,
|
'id' => $channel->id,
|
||||||
'name' => $channel->name,
|
'name' => $channel->name,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +154,7 @@ public function test_update_modifies_platform_channel_successfully(): void
|
||||||
'name' => 'Updated Channel',
|
'name' => 'Updated Channel',
|
||||||
'display_name' => 'Updated Display Name',
|
'display_name' => 'Updated Display Name',
|
||||||
'description' => 'Updated description',
|
'description' => 'Updated description',
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->putJson("/api/v1/platform-channels/{$channel->id}", $updateData);
|
$response = $this->putJson("/api/v1/platform-channels/{$channel->id}", $updateData);
|
||||||
|
|
@ -162,7 +162,7 @@ public function test_update_modifies_platform_channel_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform channel updated successfully!'
|
'message' => 'Platform channel updated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
|
|
@ -183,11 +183,11 @@ public function test_destroy_deletes_platform_channel_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform channel deleted successfully!'
|
'message' => 'Platform channel deleted successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('platform_channels', [
|
$this->assertDatabaseMissing('platform_channels', [
|
||||||
'id' => $channel->id
|
'id' => $channel->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ public function test_toggle_activates_inactive_channel(): void
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle");
|
$response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle");
|
||||||
|
|
@ -204,12 +204,12 @@ public function test_toggle_activates_inactive_channel(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform channel activated successfully!'
|
'message' => 'Platform channel activated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
'id' => $channel->id,
|
'id' => $channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,7 +218,7 @@ public function test_toggle_deactivates_active_channel(): void
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle");
|
$response = $this->postJson("/api/v1/platform-channels/{$channel->id}/toggle");
|
||||||
|
|
@ -226,12 +226,12 @@ public function test_toggle_deactivates_active_channel(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Platform channel deactivated successfully!'
|
'message' => 'Platform channel deactivated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
'id' => $channel->id,
|
'id' => $channel->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,13 +23,13 @@ public function test_index_returns_successful_response(): void
|
||||||
$feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]);
|
$feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]);
|
||||||
$channels = PlatformChannel::factory()->count(3)->create([
|
$channels = PlatformChannel::factory()->count(3)->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach ($feeds as $index => $feed) {
|
foreach ($feeds as $index => $feed) {
|
||||||
Route::factory()->create([
|
Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channels[$index]->id
|
'platform_channel_id' => $channels[$index]->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,12 +47,12 @@ public function test_index_returns_successful_response(): void
|
||||||
'priority',
|
'priority',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configurations retrieved successfully.'
|
'message' => 'Routing configurations retrieved successfully.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,14 +63,14 @@ public function test_store_creates_routing_configuration_successfully(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'priority' => 5
|
'priority' => 5,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/routing', $data);
|
$response = $this->postJson('/api/v1/routing', $data);
|
||||||
|
|
@ -86,11 +86,11 @@ public function test_store_creates_routing_configuration_successfully(): void
|
||||||
'priority',
|
'priority',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configuration created successfully!'
|
'message' => 'Routing configuration created successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('routes', [
|
$this->assertDatabaseHas('routes', [
|
||||||
|
|
@ -115,12 +115,12 @@ public function test_store_validates_feed_exists(): void
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'feed_id' => 999,
|
'feed_id' => 999,
|
||||||
'platform_channel_id' => $channel->id
|
'platform_channel_id' => $channel->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/routing', $data);
|
$response = $this->postJson('/api/v1/routing', $data);
|
||||||
|
|
@ -136,7 +136,7 @@ public function test_store_validates_platform_channel_exists(): void
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => 999
|
'platform_channel_id' => 999,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/routing', $data);
|
$response = $this->postJson('/api/v1/routing', $data);
|
||||||
|
|
@ -152,12 +152,12 @@ public function test_show_returns_routing_configuration_successfully(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$route = Route::factory()->create([
|
$route = Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id
|
'platform_channel_id' => $channel->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
||||||
|
|
@ -173,11 +173,11 @@ public function test_show_returns_routing_configuration_successfully(): void
|
||||||
'priority',
|
'priority',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]
|
],
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configuration retrieved successfully.'
|
'message' => 'Routing configuration retrieved successfully.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ public function test_show_returns_404_for_nonexistent_routing(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
||||||
|
|
@ -196,7 +196,7 @@ public function test_show_returns_404_for_nonexistent_routing(): void
|
||||||
$response->assertStatus(404)
|
$response->assertStatus(404)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Routing configuration not found.'
|
'message' => 'Routing configuration not found.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,19 +207,19 @@ public function test_update_modifies_routing_configuration_successfully(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$route = Route::factory()->create([
|
$route = Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'priority' => 1
|
'priority' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'is_active' => false,
|
'is_active' => false,
|
||||||
'priority' => 10
|
'priority' => 10,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", $updateData);
|
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", $updateData);
|
||||||
|
|
@ -227,7 +227,7 @@ public function test_update_modifies_routing_configuration_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configuration updated successfully!'
|
'message' => 'Routing configuration updated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('routes', [
|
$this->assertDatabaseHas('routes', [
|
||||||
|
|
@ -245,17 +245,17 @@ public function test_update_returns_404_for_nonexistent_routing(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
|
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(404)
|
$response->assertStatus(404)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Routing configuration not found.'
|
'message' => 'Routing configuration not found.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,12 +266,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$route = Route::factory()->create([
|
$route = Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id
|
'platform_channel_id' => $channel->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
$response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
||||||
|
|
@ -279,12 +279,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configuration deleted successfully!'
|
'message' => 'Routing configuration deleted successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('routes', [
|
$this->assertDatabaseMissing('routes', [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id
|
'platform_channel_id' => $channel->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,7 +295,7 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
$response = $this->deleteJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
||||||
|
|
@ -303,7 +303,7 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void
|
||||||
$response->assertStatus(404)
|
$response->assertStatus(404)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Routing configuration not found.'
|
'message' => 'Routing configuration not found.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,13 +314,13 @@ public function test_toggle_activates_inactive_routing(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$route = Route::factory()->create([
|
$route = Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
|
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
|
||||||
|
|
@ -328,13 +328,13 @@ public function test_toggle_activates_inactive_routing(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configuration activated successfully!'
|
'message' => 'Routing configuration activated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('routes', [
|
$this->assertDatabaseHas('routes', [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,13 +345,13 @@ public function test_toggle_deactivates_active_routing(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$route = Route::factory()->create([
|
$route = Route::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
|
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
|
||||||
|
|
@ -359,13 +359,13 @@ public function test_toggle_deactivates_active_routing(): void
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Routing configuration deactivated successfully!'
|
'message' => 'Routing configuration deactivated successfully!',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('routes', [
|
$this->assertDatabaseHas('routes', [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => false
|
'is_active' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,7 +376,7 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
|
$response = $this->postJson("/api/v1/routing/{$feed->id}/{$channel->id}/toggle");
|
||||||
|
|
@ -384,7 +384,7 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void
|
||||||
$response->assertStatus(404)
|
$response->assertStatus(404)
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Routing configuration not found.'
|
'message' => 'Routing configuration not found.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,11 +22,11 @@ public function test_index_returns_current_settings(): void
|
||||||
'publishing_approvals_enabled',
|
'publishing_approvals_enabled',
|
||||||
'article_publishing_interval',
|
'article_publishing_interval',
|
||||||
],
|
],
|
||||||
'message'
|
'message',
|
||||||
])
|
])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Settings retrieved successfully.'
|
'message' => 'Settings retrieved successfully.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ public function test_update_modifies_article_processing_setting(): void
|
||||||
'message' => 'Settings updated successfully.',
|
'message' => 'Settings updated successfully.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'article_processing_enabled' => false,
|
'article_processing_enabled' => false,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ public function test_update_modifies_publishing_approvals_setting(): void
|
||||||
'message' => 'Settings updated successfully.',
|
'message' => 'Settings updated successfully.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'publishing_approvals_enabled' => true,
|
'publishing_approvals_enabled' => true,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ public function test_update_validates_boolean_values(): void
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors([
|
->assertJsonValidationErrors([
|
||||||
'article_processing_enabled',
|
'article_processing_enabled',
|
||||||
'publishing_approvals_enabled'
|
'publishing_approvals_enabled',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ public function test_update_accepts_partial_updates(): void
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'article_processing_enabled' => true,
|
'article_processing_enabled' => true,
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Should still have structure for all settings
|
// Should still have structure for all settings
|
||||||
|
|
@ -97,7 +97,7 @@ public function test_update_accepts_partial_updates(): void
|
||||||
'article_processing_enabled',
|
'article_processing_enabled',
|
||||||
'publishing_approvals_enabled',
|
'publishing_approvals_enabled',
|
||||||
'article_publishing_interval',
|
'article_publishing_interval',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ArticleApproved;
|
use App\Events\ArticleApproved;
|
||||||
// use App\Events\ArticleReadyToPublish; // Class no longer exists
|
// use App\Events\ArticleReadyToPublish; // Class no longer exists
|
||||||
use App\Events\ExceptionLogged;
|
use App\Events\ExceptionLogged;
|
||||||
|
|
@ -18,11 +19,10 @@
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Log;
|
use App\Models\Log;
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Services\Log\LogSaver;
|
use App\Models\Setting;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use App\Services\Article\ArticleFetcher;
|
||||||
use App\Services\Article\ValidationService;
|
use App\Services\Log\LogSaver;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
|
@ -44,7 +44,7 @@ public function test_article_discovery_job_processes_successfully(): void
|
||||||
$feed = Feed::factory()->create(['is_active' => true]);
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
$logSaver = app(LogSaver::class);
|
$logSaver = app(LogSaver::class);
|
||||||
$job = new ArticleDiscoveryJob();
|
$job = new ArticleDiscoveryJob;
|
||||||
$job->handle($logSaver);
|
$job->handle($logSaver);
|
||||||
|
|
||||||
// Should dispatch individual feed jobs
|
// Should dispatch individual feed jobs
|
||||||
|
|
@ -57,7 +57,7 @@ public function test_article_discovery_for_feed_job_processes_feed(): void
|
||||||
|
|
||||||
$feed = Feed::factory()->create([
|
$feed = Feed::factory()->create([
|
||||||
'url' => 'https://example.com/feed',
|
'url' => 'https://example.com/feed',
|
||||||
'is_active' => true
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Mock the ArticleFetcher service in the container
|
// Mock the ArticleFetcher service in the container
|
||||||
|
|
@ -94,10 +94,9 @@ public function test_sync_channel_posts_job_processes_successfully(): void
|
||||||
$this->assertTrue(true);
|
$this->assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function test_publish_next_article_job_has_correct_configuration(): void
|
public function test_publish_next_article_job_has_correct_configuration(): void
|
||||||
{
|
{
|
||||||
$job = new PublishNextArticleJob();
|
$job = new PublishNextArticleJob;
|
||||||
|
|
||||||
$this->assertEquals('publishing', $job->queue);
|
$this->assertEquals('publishing', $job->queue);
|
||||||
$this->assertInstanceOf(PublishNextArticleJob::class, $job);
|
$this->assertInstanceOf(PublishNextArticleJob::class, $job);
|
||||||
|
|
@ -164,12 +163,12 @@ public function test_exception_logged_event_is_dispatched(): void
|
||||||
$log = Log::factory()->create([
|
$log = Log::factory()->create([
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
'message' => 'Test error',
|
'message' => 'Test error',
|
||||||
'context' => json_encode(['key' => 'value'])
|
'context' => json_encode(['key' => 'value']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
event(new ExceptionLogged($log));
|
event(new ExceptionLogged($log));
|
||||||
|
|
||||||
Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) use ($log) {
|
Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) {
|
||||||
return $event->log->message === 'Test error';
|
return $event->log->message === 'Test error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +194,7 @@ public function test_validate_article_listener_processes_new_article(): void
|
||||||
->andReturn([
|
->andReturn([
|
||||||
'title' => 'Belgian News',
|
'title' => 'Belgian News',
|
||||||
'description' => 'News from Belgium',
|
'description' => 'News from Belgium',
|
||||||
'full_article' => 'This is a test article about Belgium and Belgian politics.'
|
'full_article' => 'This is a test article about Belgium and Belgian politics.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$listener = app(ValidateArticleListener::class);
|
$listener = app(ValidateArticleListener::class);
|
||||||
|
|
@ -248,10 +247,10 @@ public function test_log_exception_to_database_listener_creates_log(): void
|
||||||
$log = Log::factory()->create([
|
$log = Log::factory()->create([
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
'message' => 'Test exception message',
|
'message' => 'Test exception message',
|
||||||
'context' => json_encode(['error' => 'details'])
|
'context' => json_encode(['error' => 'details']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$listener = new LogExceptionToDatabase();
|
$listener = new LogExceptionToDatabase;
|
||||||
$exception = new \Exception('Test exception message');
|
$exception = new \Exception('Test exception message');
|
||||||
$event = new ExceptionOccurred($exception, \App\Enums\LogLevelEnum::ERROR, 'Test exception message');
|
$event = new ExceptionOccurred($exception, \App\Enums\LogLevelEnum::ERROR, 'Test exception message');
|
||||||
|
|
||||||
|
|
@ -259,7 +258,7 @@ public function test_log_exception_to_database_listener_creates_log(): void
|
||||||
|
|
||||||
$this->assertDatabaseHas('logs', [
|
$this->assertDatabaseHas('logs', [
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
'message' => 'Test exception message'
|
'message' => 'Test exception message',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$savedLog = Log::where('message', 'Test exception message')->first();
|
$savedLog = Log::where('message', 'Test exception message')->first();
|
||||||
|
|
@ -270,6 +269,9 @@ public function test_log_exception_to_database_listener_creates_log(): void
|
||||||
public function test_event_listener_registration_works(): void
|
public function test_event_listener_registration_works(): void
|
||||||
{
|
{
|
||||||
// Test that events are properly bound to listeners
|
// Test that events are properly bound to listeners
|
||||||
|
$listeners = Event::getListeners(ActionPerformed::class);
|
||||||
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
$listeners = Event::getListeners(NewArticleFetched::class);
|
$listeners = Event::getListeners(NewArticleFetched::class);
|
||||||
$this->assertNotEmpty($listeners);
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
|
|
@ -287,7 +289,7 @@ public function test_event_listener_registration_works(): void
|
||||||
|
|
||||||
public function test_job_retry_configuration(): void
|
public function test_job_retry_configuration(): void
|
||||||
{
|
{
|
||||||
$job = new PublishNextArticleJob();
|
$job = new PublishNextArticleJob;
|
||||||
|
|
||||||
// Test that job has unique configuration
|
// Test that job has unique configuration
|
||||||
$this->assertObjectHasProperty('uniqueFor', $job);
|
$this->assertObjectHasProperty('uniqueFor', $job);
|
||||||
|
|
@ -300,9 +302,9 @@ public function test_job_queue_configuration(): void
|
||||||
$channel = PlatformChannel::factory()->create();
|
$channel = PlatformChannel::factory()->create();
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
$discoveryJob = new ArticleDiscoveryJob();
|
$discoveryJob = new ArticleDiscoveryJob;
|
||||||
$feedJob = new ArticleDiscoveryForFeedJob($feed);
|
$feedJob = new ArticleDiscoveryForFeedJob($feed);
|
||||||
$publishJob = new PublishNextArticleJob();
|
$publishJob = new PublishNextArticleJob;
|
||||||
$syncJob = new SyncChannelPostsJob($channel);
|
$syncJob = new SyncChannelPostsJob($channel);
|
||||||
|
|
||||||
// Test queue assignments
|
// Test queue assignments
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue