Compare commits
No commits in common. "0123e20b1d93658a53e6d417afb5c436715dc32c" and "3ed49cfbbe2ce36eaaa2eec9ae79e971e2ec5b31" have entirely different histories.
0123e20b1d
...
3ed49cfbbe
28 changed files with 706 additions and 824 deletions
28
.env.testing
28
.env.testing
|
|
@ -1,28 +0,0 @@
|
||||||
APP_NAME=Laravel
|
|
||||||
APP_ENV=testing
|
|
||||||
APP_KEY=base64:5VABFQKtzx6flRFn7rQUQYI/G8xLnkUSYPVaYz2s/4M=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://localhost
|
|
||||||
|
|
||||||
APP_MAINTENANCE_DRIVER=file
|
|
||||||
|
|
||||||
BCRYPT_ROUNDS=4
|
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
LOG_STACK=single
|
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
|
||||||
DB_DATABASE=:memory:
|
|
||||||
|
|
||||||
SESSION_DRIVER=array
|
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
|
|
||||||
CACHE_STORE=array
|
|
||||||
|
|
||||||
MAIL_MAILER=array
|
|
||||||
|
|
||||||
PULSE_ENABLED=false
|
|
||||||
TELESCOPE_ENABLED=false
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions;
|
|
||||||
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class CreateChannelAction
|
|
||||||
{
|
|
||||||
public function execute(string $name, int $platformInstanceId, ?int $languageId = null, ?string $description = null): PlatformChannel
|
|
||||||
{
|
|
||||||
$platformInstance = PlatformInstance::findOrFail($platformInstanceId);
|
|
||||||
|
|
||||||
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
|
||||||
->where('is_active', true)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($activeAccounts->isEmpty()) {
|
|
||||||
throw new RuntimeException('No active platform accounts found for this instance. Please create a platform account first.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return DB::transaction(function () use ($name, $platformInstanceId, $languageId, $description, $activeAccounts) {
|
|
||||||
$channel = PlatformChannel::create([
|
|
||||||
'platform_instance_id' => $platformInstanceId,
|
|
||||||
'channel_id' => $name,
|
|
||||||
'name' => $name,
|
|
||||||
'display_name' => ucfirst($name),
|
|
||||||
'description' => $description,
|
|
||||||
'language_id' => $languageId,
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Attach only the first active account — additional accounts can be linked via the channel management UI
|
|
||||||
$channel->platformAccounts()->attach($activeAccounts->first()->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 1,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $channel->load('platformAccounts');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions;
|
|
||||||
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\Language;
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
class CreateFeedAction
|
|
||||||
{
|
|
||||||
public function execute(string $name, string $provider, int $languageId, ?string $description = null): Feed
|
|
||||||
{
|
|
||||||
$language = Language::findOrFail($languageId);
|
|
||||||
$langCode = $language->short_code;
|
|
||||||
|
|
||||||
$url = config("feed.providers.{$provider}.languages.{$langCode}.url");
|
|
||||||
|
|
||||||
if (!$url) {
|
|
||||||
throw new InvalidArgumentException("Invalid provider and language combination: {$provider}/{$langCode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$providerConfig = config("feed.providers.{$provider}");
|
|
||||||
|
|
||||||
return Feed::firstOrCreate(
|
|
||||||
['url' => $url],
|
|
||||||
[
|
|
||||||
'name' => $name,
|
|
||||||
'type' => $providerConfig['type'] ?? 'website',
|
|
||||||
'provider' => $provider,
|
|
||||||
'language_id' => $languageId,
|
|
||||||
'description' => $description,
|
|
||||||
'is_active' => true,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions;
|
|
||||||
|
|
||||||
use App\Exceptions\PlatformAuthException;
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use App\Services\Auth\LemmyAuthService;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class CreatePlatformAccountAction
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly LemmyAuthService $lemmyAuthService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws PlatformAuthException
|
|
||||||
*/
|
|
||||||
public function execute(string $instanceDomain, string $username, string $password, string $platform = 'lemmy'): PlatformAccount
|
|
||||||
{
|
|
||||||
$fullInstanceUrl = 'https://' . $instanceDomain;
|
|
||||||
|
|
||||||
// Authenticate first — if this fails, no records are created
|
|
||||||
$authResponse = $this->lemmyAuthService->authenticate($fullInstanceUrl, $username, $password);
|
|
||||||
|
|
||||||
return DB::transaction(function () use ($fullInstanceUrl, $instanceDomain, $username, $password, $platform, $authResponse) {
|
|
||||||
$platformInstance = PlatformInstance::firstOrCreate([
|
|
||||||
'url' => $fullInstanceUrl,
|
|
||||||
'platform' => $platform,
|
|
||||||
], [
|
|
||||||
'name' => ucfirst($instanceDomain),
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return PlatformAccount::create([
|
|
||||||
'platform' => $platform,
|
|
||||||
'instance_url' => $fullInstanceUrl,
|
|
||||||
'username' => $username,
|
|
||||||
'password' => $password,
|
|
||||||
'settings' => [
|
|
||||||
'display_name' => $authResponse['person_view']['person']['display_name'] ?? null,
|
|
||||||
'description' => $authResponse['person_view']['person']['bio'] ?? null,
|
|
||||||
'person_id' => $authResponse['person_view']['person']['id'] ?? null,
|
|
||||||
'platform_instance_id' => $platformInstance->id,
|
|
||||||
'api_token' => $authResponse['jwt'] ?? null,
|
|
||||||
],
|
|
||||||
'is_active' => true,
|
|
||||||
'status' => 'active',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions;
|
|
||||||
|
|
||||||
use App\Models\Route;
|
|
||||||
|
|
||||||
class CreateRouteAction
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create a route or return an existing one for the same feed+channel pair.
|
|
||||||
* When a route already exists, the provided priority and isActive values are ignored.
|
|
||||||
*/
|
|
||||||
public function execute(int $feedId, int $platformChannelId, int $priority = 0, bool $isActive = true): Route
|
|
||||||
{
|
|
||||||
return Route::firstOrCreate(
|
|
||||||
[
|
|
||||||
'feed_id' => $feedId,
|
|
||||||
'platform_channel_id' => $platformChannelId,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'priority' => $priority,
|
|
||||||
'is_active' => $isActive,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,15 +2,12 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Actions\CreateFeedAction;
|
|
||||||
use App\Http\Requests\StoreFeedRequest;
|
use App\Http\Requests\StoreFeedRequest;
|
||||||
use App\Http\Requests\UpdateFeedRequest;
|
use App\Http\Requests\UpdateFeedRequest;
|
||||||
use App\Http\Resources\FeedResource;
|
use App\Http\Resources\FeedResource;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
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;
|
||||||
|
|
||||||
class FeedsController extends BaseController
|
class FeedsController extends BaseController
|
||||||
|
|
@ -44,26 +41,32 @@ public function index(Request $request): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Store a newly created feed
|
* Store a newly created feed
|
||||||
*/
|
*/
|
||||||
public function store(StoreFeedRequest $request, CreateFeedAction $createFeedAction): JsonResponse
|
public function store(StoreFeedRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
|
|
||||||
$feed = $createFeedAction->execute(
|
// Map provider to URL and set type
|
||||||
$validated['name'],
|
$providers = [
|
||||||
$validated['provider'],
|
'vrt' => new \App\Services\Parsers\VrtHomepageParserAdapter(),
|
||||||
$validated['language_id'],
|
'belga' => new \App\Services\Parsers\BelgaHomepageParserAdapter(),
|
||||||
$validated['description'] ?? null,
|
];
|
||||||
);
|
|
||||||
|
$adapter = $providers[$validated['provider']];
|
||||||
|
$validated['url'] = $adapter->getHomepageUrl();
|
||||||
|
$validated['type'] = 'website';
|
||||||
|
|
||||||
|
$feed = Feed::create($validated);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed),
|
new FeedResource($feed),
|
||||||
'Feed created successfully!',
|
'Feed created successfully!',
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendError($e->getMessage(), [], 422);
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to create feed: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to create feed: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +99,7 @@ public function update(UpdateFeedRequest $request, Feed $feed): JsonResponse
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendValidationError($e->errors());
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to update feed: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update feed: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +116,7 @@ public function destroy(Feed $feed): JsonResponse
|
||||||
null,
|
null,
|
||||||
'Feed deleted successfully!'
|
'Feed deleted successfully!'
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to delete feed: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to delete feed: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +136,7 @@ public function toggle(Feed $feed): JsonResponse
|
||||||
new FeedResource($feed->fresh()),
|
new FeedResource($feed->fresh()),
|
||||||
"Feed {$status} successfully!"
|
"Feed {$status} successfully!"
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to toggle feed status: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to toggle feed status: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreFeedRequest;
|
||||||
|
use App\Http\Resources\FeedResource;
|
||||||
|
use App\Http\Resources\PlatformAccountResource;
|
||||||
|
use App\Http\Resources\PlatformChannelResource;
|
||||||
|
use App\Http\Resources\RouteResource;
|
||||||
|
use App\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Language;
|
use App\Models\Language;
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
|
|
@ -9,10 +15,18 @@
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use App\Services\Auth\LemmyAuthService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class OnboardingController extends BaseController
|
class OnboardingController extends BaseController
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LemmyAuthService $lemmyAuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get onboarding status - whether user needs onboarding
|
* Get onboarding status - whether user needs onboarding
|
||||||
*/
|
*/
|
||||||
|
|
@ -97,6 +111,233 @@ public function options(): JsonResponse
|
||||||
], 'Onboarding options retrieved successfully.');
|
], 'Onboarding options retrieved successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create platform account for onboarding
|
||||||
|
*/
|
||||||
|
public function createPlatform(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'instance_url' => 'required|string|max:255|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/',
|
||||||
|
'username' => 'required|string|max:255',
|
||||||
|
'password' => 'required|string|min:6',
|
||||||
|
'platform' => 'required|in:lemmy',
|
||||||
|
], [
|
||||||
|
'instance_url.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new ValidationException($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
// Normalize the instance URL - prepend https:// if needed
|
||||||
|
$instanceDomain = $validated['instance_url'];
|
||||||
|
$fullInstanceUrl = 'https://' . $instanceDomain;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create or get platform instance
|
||||||
|
$platformInstance = PlatformInstance::firstOrCreate([
|
||||||
|
'url' => $fullInstanceUrl,
|
||||||
|
'platform' => $validated['platform'],
|
||||||
|
], [
|
||||||
|
'name' => ucfirst($instanceDomain),
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Authenticate with Lemmy API using the full URL
|
||||||
|
$authResponse = $this->lemmyAuthService->authenticate(
|
||||||
|
$fullInstanceUrl,
|
||||||
|
$validated['username'],
|
||||||
|
$validated['password']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create platform account with the current schema
|
||||||
|
$platformAccount = PlatformAccount::create([
|
||||||
|
'platform' => $validated['platform'],
|
||||||
|
'instance_url' => $fullInstanceUrl,
|
||||||
|
'username' => $validated['username'],
|
||||||
|
'password' => $validated['password'],
|
||||||
|
'settings' => [
|
||||||
|
'display_name' => $authResponse['person_view']['person']['display_name'] ?? null,
|
||||||
|
'description' => $authResponse['person_view']['person']['bio'] ?? null,
|
||||||
|
'person_id' => $authResponse['person_view']['person']['id'] ?? null,
|
||||||
|
'platform_instance_id' => $platformInstance->id,
|
||||||
|
'api_token' => $authResponse['jwt'] ?? null, // Store JWT in settings for now
|
||||||
|
],
|
||||||
|
'is_active' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformAccountResource($platformAccount),
|
||||||
|
'Platform account created successfully.'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\App\Exceptions\PlatformAuthException $e) {
|
||||||
|
// Check if it's a rate limit error
|
||||||
|
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
||||||
|
return $this->sendError($e->getMessage(), [], 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
$message = 'Unable to connect to the Lemmy instance. Please check the URL and try again.';
|
||||||
|
|
||||||
|
// If it's a network/connection issue, provide a more specific message
|
||||||
|
if (str_contains(strtolower($e->getMessage()), 'connection') ||
|
||||||
|
str_contains(strtolower($e->getMessage()), 'network') ||
|
||||||
|
str_contains(strtolower($e->getMessage()), 'timeout')) {
|
||||||
|
$message = 'Connection failed. Please check the instance URL and your internet connection.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendError($message, [], 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create feed for onboarding
|
||||||
|
*/
|
||||||
|
public function createFeed(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'provider' => 'required|in:belga,vrt',
|
||||||
|
'language_id' => 'required|exists:languages,id',
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new ValidationException($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
// Map provider to preset URL and type as required by onboarding tests
|
||||||
|
$provider = $validated['provider'];
|
||||||
|
$url = null;
|
||||||
|
$type = 'website';
|
||||||
|
if ($provider === 'vrt') {
|
||||||
|
$url = 'https://www.vrt.be/vrtnws/en/';
|
||||||
|
} elseif ($provider === 'belga') {
|
||||||
|
$url = 'https://www.belganewsagency.eu/';
|
||||||
|
}
|
||||||
|
|
||||||
|
$feed = Feed::firstOrCreate(
|
||||||
|
['url' => $url],
|
||||||
|
[
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'type' => $type,
|
||||||
|
'provider' => $provider,
|
||||||
|
'language_id' => $validated['language_id'],
|
||||||
|
'description' => $validated['description'] ?? null,
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new FeedResource($feed->load('language')),
|
||||||
|
'Feed created successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create channel for onboarding
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function createChannel(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
|
'language_id' => 'required|exists:languages,id',
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new ValidationException($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
// Get the platform instance to check for active accounts
|
||||||
|
$platformInstance = PlatformInstance::findOrFail($validated['platform_instance_id']);
|
||||||
|
|
||||||
|
// Check if there are active platform accounts for this instance
|
||||||
|
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
||||||
|
->where('is_active', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($activeAccounts->isEmpty()) {
|
||||||
|
return $this->sendError(
|
||||||
|
'Cannot create channel: No active platform accounts found for this instance. Please create a platform account first.',
|
||||||
|
[],
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel = PlatformChannel::create([
|
||||||
|
'platform_instance_id' => $validated['platform_instance_id'],
|
||||||
|
'channel_id' => $validated['name'], // For Lemmy, this is the community name
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'display_name' => ucfirst($validated['name']),
|
||||||
|
'description' => $validated['description'] ?? null,
|
||||||
|
'language_id' => $validated['language_id'],
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Automatically attach the first active account to the channel
|
||||||
|
$firstAccount = $activeAccounts->first();
|
||||||
|
$channel->platformAccounts()->attach($firstAccount->id, [
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 1,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new PlatformChannelResource($channel->load(['platformInstance', 'language', 'platformAccounts'])),
|
||||||
|
'Channel created successfully and linked to platform account.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create route for onboarding
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function createRoute(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'priority' => 'nullable|integer|min:1|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new ValidationException($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $validated['feed_id'],
|
||||||
|
'platform_channel_id' => $validated['platform_channel_id'],
|
||||||
|
'priority' => $validated['priority'] ?? 50,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Trigger article discovery when the first route is created during onboarding
|
||||||
|
// This ensures articles start being fetched immediately after setup
|
||||||
|
ArticleDiscoveryJob::dispatch();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
new RouteResource($route->load(['feed', 'platformChannel'])),
|
||||||
|
'Route created successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark onboarding as complete
|
* Mark onboarding as complete
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Actions\CreatePlatformAccountAction;
|
use App\Enums\PlatformEnum;
|
||||||
use App\Exceptions\PlatformAuthException;
|
|
||||||
use App\Http\Requests\StorePlatformAccountRequest;
|
|
||||||
use App\Http\Resources\PlatformAccountResource;
|
use App\Http\Resources\PlatformAccountResource;
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
use Exception;
|
use App\Models\PlatformInstance;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
@ -32,30 +30,46 @@ public function index(): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Store a newly created platform account
|
* Store a newly created platform account
|
||||||
*/
|
*/
|
||||||
public function store(StorePlatformAccountRequest $request, CreatePlatformAccountAction $action): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validated();
|
$validated = $request->validate([
|
||||||
|
'platform' => 'required|in:lemmy,mastodon,reddit',
|
||||||
|
'instance_url' => 'required|url',
|
||||||
|
'username' => 'required|string|max:255',
|
||||||
|
'password' => 'required|string',
|
||||||
|
'settings' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
$account = $action->execute(
|
// Create or find platform instance
|
||||||
$validated['instance_domain'],
|
$platformEnum = PlatformEnum::from($validated['platform']);
|
||||||
$validated['username'],
|
$instance = PlatformInstance::firstOrCreate([
|
||||||
$validated['password'],
|
'platform' => $platformEnum,
|
||||||
$validated['platform'],
|
'url' => $validated['instance_url'],
|
||||||
);
|
], [
|
||||||
|
'name' => parse_url($validated['instance_url'], PHP_URL_HOST),
|
||||||
|
'description' => ucfirst($validated['platform']) . ' instance',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$account = PlatformAccount::create($validated);
|
||||||
|
|
||||||
|
// If this is the first account for this platform, make it active
|
||||||
|
if (!PlatformAccount::where('platform', $validated['platform'])
|
||||||
|
->where('is_active', true)
|
||||||
|
->exists()) {
|
||||||
|
$account->setAsActive();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new PlatformAccountResource($account),
|
new PlatformAccountResource($account),
|
||||||
'Platform account created successfully!',
|
'Platform account created successfully!',
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
} catch (PlatformAuthException $e) {
|
} catch (ValidationException $e) {
|
||||||
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
return $this->sendValidationError($e->errors());
|
||||||
return $this->sendError($e->getMessage(), [], 429);
|
} catch (\Exception $e) {
|
||||||
}
|
return $this->sendError('Failed to create platform account: ' . $e->getMessage(), [], 500);
|
||||||
return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $this->sendError('Unable to connect to the Lemmy instance. Please check the URL and try again.', [], 422);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +110,7 @@ public function update(Request $request, PlatformAccount $platformAccount): Json
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendValidationError($e->errors());
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to update platform account: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update platform account: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +127,7 @@ public function destroy(PlatformAccount $platformAccount): JsonResponse
|
||||||
null,
|
null,
|
||||||
'Platform account deleted successfully!'
|
'Platform account deleted successfully!'
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to delete platform account: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to delete platform account: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +144,7 @@ public function setActive(PlatformAccount $platformAccount): JsonResponse
|
||||||
new PlatformAccountResource($platformAccount->fresh()),
|
new PlatformAccountResource($platformAccount->fresh()),
|
||||||
"Set {$platformAccount->username}@{$platformAccount->instance_url} as active for {$platformAccount->platform->value}!"
|
"Set {$platformAccount->username}@{$platformAccount->instance_url} as active for {$platformAccount->platform->value}!"
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to set platform account as active: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to set platform account as active: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,12 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Actions\CreateChannelAction;
|
|
||||||
use App\Http\Requests\StorePlatformChannelRequest;
|
|
||||||
use App\Http\Resources\PlatformChannelResource;
|
use App\Http\Resources\PlatformChannelResource;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
use Exception;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class PlatformChannelsController extends BaseController
|
class PlatformChannelsController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
@ -34,26 +30,55 @@ public function index(): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Store a newly created platform channel
|
* Store a newly created platform channel
|
||||||
*/
|
*/
|
||||||
public function store(StorePlatformChannelRequest $request, CreateChannelAction $createChannelAction): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validated();
|
$validated = $request->validate([
|
||||||
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
|
'channel_id' => 'required|string|max:255',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'display_name' => 'nullable|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
$channel = $createChannelAction->execute(
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
$validated['name'],
|
|
||||||
$validated['platform_instance_id'],
|
// Get the platform instance to check for active accounts
|
||||||
$validated['language_id'] ?? null,
|
$platformInstance = \App\Models\PlatformInstance::findOrFail($validated['platform_instance_id']);
|
||||||
$validated['description'] ?? null,
|
|
||||||
|
// Check if there are active platform accounts for this instance
|
||||||
|
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
||||||
|
->where('is_active', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($activeAccounts->isEmpty()) {
|
||||||
|
return $this->sendError(
|
||||||
|
'Cannot create channel: No active platform accounts found for this instance. Please create a platform account first.',
|
||||||
|
[],
|
||||||
|
422
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel = PlatformChannel::create($validated);
|
||||||
|
|
||||||
|
// Automatically attach the first active account to the channel
|
||||||
|
$firstAccount = $activeAccounts->first();
|
||||||
|
$channel->platformAccounts()->attach($firstAccount->id, [
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 1,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new PlatformChannelResource($channel->load(['platformInstance', 'platformAccounts'])),
|
new PlatformChannelResource($channel->load(['platformInstance', 'platformAccounts'])),
|
||||||
'Platform channel created successfully and linked to platform account!',
|
'Platform channel created successfully and linked to platform account!',
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
} catch (RuntimeException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendError($e->getMessage(), [], 422);
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to create platform channel: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to create platform channel: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +115,7 @@ public function update(Request $request, PlatformChannel $platformChannel): Json
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendValidationError($e->errors());
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to update platform channel: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update platform channel: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +132,7 @@ public function destroy(PlatformChannel $platformChannel): JsonResponse
|
||||||
null,
|
null,
|
||||||
'Platform channel deleted successfully!'
|
'Platform channel deleted successfully!'
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to delete platform channel: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to delete platform channel: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +152,7 @@ public function toggle(PlatformChannel $channel): JsonResponse
|
||||||
new PlatformChannelResource($channel->fresh(['platformInstance', 'platformAccounts'])),
|
new PlatformChannelResource($channel->fresh(['platformInstance', 'platformAccounts'])),
|
||||||
"Platform channel {$status} successfully!"
|
"Platform channel {$status} successfully!"
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to toggle platform channel status: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to toggle platform channel status: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +189,7 @@ public function attachAccount(PlatformChannel $channel, Request $request): JsonR
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendValidationError($e->errors());
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to attach platform account: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to attach platform account: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +210,7 @@ public function detachAccount(PlatformChannel $channel, PlatformAccount $account
|
||||||
new PlatformChannelResource($channel->fresh(['platformInstance', 'platformAccounts'])),
|
new PlatformChannelResource($channel->fresh(['platformInstance', 'platformAccounts'])),
|
||||||
'Platform account detached from channel successfully!'
|
'Platform account detached from channel successfully!'
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to detach platform account: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to detach platform account: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +242,7 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendValidationError($e->errors());
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Actions\CreateRouteAction;
|
|
||||||
use App\Http\Requests\StoreRouteRequest;
|
|
||||||
use App\Http\Resources\RouteResource;
|
use App\Http\Resources\RouteResource;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
use Exception;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
@ -34,24 +31,29 @@ public function index(): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Store a newly created routing configuration
|
* Store a newly created routing configuration
|
||||||
*/
|
*/
|
||||||
public function store(StoreRouteRequest $request, CreateRouteAction $createRouteAction): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validated();
|
$validated = $request->validate([
|
||||||
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
$route = $createRouteAction->execute(
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
$validated['feed_id'],
|
$validated['priority'] = $validated['priority'] ?? 0;
|
||||||
$validated['platform_channel_id'],
|
|
||||||
$validated['priority'] ?? 0,
|
$route = Route::create($validated);
|
||||||
$validated['is_active'] ?? true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->load(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->load(['feed', 'platformChannel', 'keywords'])),
|
||||||
'Routing configuration created successfully!',
|
'Routing configuration created successfully!',
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to create routing configuration: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to create routing configuration: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +104,7 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel):
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return $this->sendValidationError($e->errors());
|
return $this->sendValidationError($e->errors());
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to update routing configuration: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update routing configuration: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +129,7 @@ public function destroy(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
null,
|
null,
|
||||||
'Routing configuration deleted successfully!'
|
'Routing configuration deleted successfully!'
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to delete routing configuration: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to delete routing configuration: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +157,7 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
|
||||||
"Routing configuration {$status} successfully!"
|
"Routing configuration {$status} successfully!"
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->sendError('Failed to toggle routing configuration status: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to toggle routing configuration status: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StorePlatformAccountRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'platform' => 'required|in:lemmy',
|
|
||||||
'instance_domain' => 'required|string|max:255|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/',
|
|
||||||
'username' => 'required|string|max:255',
|
|
||||||
'password' => 'required|string',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'instance_domain.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StorePlatformChannelRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'platform_instance_id' => 'required|exists:platform_instances,id',
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'language_id' => 'nullable|exists:languages,id',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreRouteRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'feed_id' => 'required|exists:feeds,id',
|
|
||||||
'platform_channel_id' => 'required|exists:platform_channels,id',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'priority' => 'nullable|integer|min:0',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Actions\CreateChannelAction;
|
|
||||||
use App\Actions\CreateFeedAction;
|
|
||||||
use App\Actions\CreatePlatformAccountAction;
|
|
||||||
use App\Actions\CreateRouteAction;
|
|
||||||
use App\Exceptions\PlatformAuthException;
|
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
use App\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Jobs\SyncChannelPostsJob;
|
use App\Jobs\SyncChannelPostsJob;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
|
|
@ -16,11 +11,10 @@
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use App\Services\Auth\LemmyAuthService;
|
||||||
use App\Services\OnboardingService;
|
use App\Services\OnboardingService;
|
||||||
use Exception;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use InvalidArgumentException;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class Onboarding extends Component
|
class Onboarding extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -53,24 +47,13 @@ class Onboarding extends Component
|
||||||
// State
|
// State
|
||||||
public array $formErrors = [];
|
public array $formErrors = [];
|
||||||
public bool $isLoading = false;
|
public bool $isLoading = false;
|
||||||
#[\Livewire\Attributes\Locked]
|
private ?int $previousChannelLanguageId = null;
|
||||||
public ?int $previousChannelLanguageId = null;
|
|
||||||
|
|
||||||
protected CreatePlatformAccountAction $createPlatformAccountAction;
|
protected LemmyAuthService $lemmyAuthService;
|
||||||
protected CreateFeedAction $createFeedAction;
|
|
||||||
protected CreateChannelAction $createChannelAction;
|
|
||||||
protected CreateRouteAction $createRouteAction;
|
|
||||||
|
|
||||||
public function boot(
|
public function boot(LemmyAuthService $lemmyAuthService): void
|
||||||
CreatePlatformAccountAction $createPlatformAccountAction,
|
{
|
||||||
CreateFeedAction $createFeedAction,
|
$this->lemmyAuthService = $lemmyAuthService;
|
||||||
CreateChannelAction $createChannelAction,
|
|
||||||
CreateRouteAction $createRouteAction,
|
|
||||||
): void {
|
|
||||||
$this->createPlatformAccountAction = $createPlatformAccountAction;
|
|
||||||
$this->createFeedAction = $createFeedAction;
|
|
||||||
$this->createChannelAction = $createChannelAction;
|
|
||||||
$this->createRouteAction = $createRouteAction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
|
|
@ -163,13 +146,42 @@ public function createPlatformAccount(): void
|
||||||
'instanceUrl.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)',
|
'instanceUrl.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$fullInstanceUrl = 'https://' . $this->instanceUrl;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$platformAccount = $this->createPlatformAccountAction->execute(
|
// Authenticate with Lemmy API first (before creating any records)
|
||||||
$this->instanceUrl,
|
$authResponse = $this->lemmyAuthService->authenticate(
|
||||||
|
$fullInstanceUrl,
|
||||||
$this->username,
|
$this->username,
|
||||||
$this->password,
|
$this->password
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Only create platform instance after successful authentication
|
||||||
|
$platformInstance = PlatformInstance::firstOrCreate([
|
||||||
|
'url' => $fullInstanceUrl,
|
||||||
|
'platform' => 'lemmy',
|
||||||
|
], [
|
||||||
|
'name' => ucfirst($this->instanceUrl),
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create platform account
|
||||||
|
$platformAccount = PlatformAccount::create([
|
||||||
|
'platform' => 'lemmy',
|
||||||
|
'instance_url' => $fullInstanceUrl,
|
||||||
|
'username' => $this->username,
|
||||||
|
'password' => Crypt::encryptString($this->password),
|
||||||
|
'settings' => [
|
||||||
|
'display_name' => $authResponse['person_view']['person']['display_name'] ?? null,
|
||||||
|
'description' => $authResponse['person_view']['person']['bio'] ?? null,
|
||||||
|
'person_id' => $authResponse['person_view']['person']['id'] ?? null,
|
||||||
|
'platform_instance_id' => $platformInstance->id,
|
||||||
|
'api_token' => $authResponse['jwt'] ?? null,
|
||||||
|
],
|
||||||
|
'is_active' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
$this->existingAccount = [
|
$this->existingAccount = [
|
||||||
'id' => $platformAccount->id,
|
'id' => $platformAccount->id,
|
||||||
'username' => $platformAccount->username,
|
'username' => $platformAccount->username,
|
||||||
|
|
@ -177,7 +189,7 @@ public function createPlatformAccount(): void
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->nextStep();
|
$this->nextStep();
|
||||||
} catch (PlatformAuthException $e) {
|
} catch (\App\Exceptions\PlatformAuthException $e) {
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
if (str_contains($message, 'Rate limited by')) {
|
if (str_contains($message, 'Rate limited by')) {
|
||||||
$this->formErrors['general'] = $message;
|
$this->formErrors['general'] = $message;
|
||||||
|
|
@ -186,9 +198,9 @@ public function createPlatformAccount(): void
|
||||||
} else {
|
} else {
|
||||||
$this->formErrors['general'] = 'Invalid username or password. Please check your credentials and try again.';
|
$this->formErrors['general'] = 'Invalid username or password. Please check your credentials and try again.';
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
logger()->error('Lemmy platform account creation failed', [
|
logger()->error('Lemmy platform account creation failed', [
|
||||||
'instance_url' => 'https://' . $this->instanceUrl,
|
'instance_url' => $fullInstanceUrl,
|
||||||
'username' => $this->username,
|
'username' => $this->username,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'class' => get_class($e),
|
'class' => get_class($e),
|
||||||
|
|
@ -215,17 +227,35 @@ public function createFeed(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->createFeedAction->execute(
|
// Get language short code
|
||||||
$this->feedName,
|
$language = Language::find($this->feedLanguageId);
|
||||||
$this->feedProvider,
|
$langCode = $language->short_code;
|
||||||
$this->feedLanguageId,
|
|
||||||
$this->feedDescription ?: null,
|
// Look up URL from config
|
||||||
|
$url = config("feed.providers.{$this->feedProvider}.languages.{$langCode}.url");
|
||||||
|
|
||||||
|
if (!$url) {
|
||||||
|
$this->formErrors['general'] = 'Invalid provider and language combination.';
|
||||||
|
$this->isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$providerConfig = config("feed.providers.{$this->feedProvider}");
|
||||||
|
|
||||||
|
Feed::firstOrCreate(
|
||||||
|
['url' => $url],
|
||||||
|
[
|
||||||
|
'name' => $this->feedName,
|
||||||
|
'type' => $providerConfig['type'] ?? 'website',
|
||||||
|
'provider' => $this->feedProvider,
|
||||||
|
'language_id' => $this->feedLanguageId,
|
||||||
|
'description' => $this->feedDescription ?: null,
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->nextStep();
|
$this->nextStep();
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (\Exception $e) {
|
||||||
$this->formErrors['general'] = 'Invalid provider and language combination.';
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->formErrors['general'] = 'Failed to create feed. Please try again.';
|
$this->formErrors['general'] = 'Failed to create feed. Please try again.';
|
||||||
} finally {
|
} finally {
|
||||||
$this->isLoading = false;
|
$this->isLoading = false;
|
||||||
|
|
@ -255,20 +285,42 @@ public function createChannel(): void
|
||||||
$this->previousChannelLanguageId = $this->channelLanguageId;
|
$this->previousChannelLanguageId = $this->channelLanguageId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$channel = $this->createChannelAction->execute(
|
$platformInstance = PlatformInstance::findOrFail($this->platformInstanceId);
|
||||||
$this->channelName,
|
|
||||||
$this->platformInstanceId,
|
// Check for active platform accounts
|
||||||
$this->channelLanguageId,
|
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
||||||
$this->channelDescription ?: null,
|
->where('is_active', true)
|
||||||
);
|
->get();
|
||||||
|
|
||||||
|
if ($activeAccounts->isEmpty()) {
|
||||||
|
$this->formErrors['general'] = 'No active platform accounts found for this instance. Please create a platform account first.';
|
||||||
|
$this->isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel = PlatformChannel::create([
|
||||||
|
'platform_instance_id' => $this->platformInstanceId,
|
||||||
|
'channel_id' => $this->channelName,
|
||||||
|
'name' => $this->channelName,
|
||||||
|
'display_name' => ucfirst($this->channelName),
|
||||||
|
'description' => $this->channelDescription ?: null,
|
||||||
|
'language_id' => $this->channelLanguageId,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Attach first active account
|
||||||
|
$channel->platformAccounts()->attach($activeAccounts->first()->id, [
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 1,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
// Sync existing posts from this channel for duplicate detection
|
// Sync existing posts from this channel for duplicate detection
|
||||||
SyncChannelPostsJob::dispatch($channel);
|
SyncChannelPostsJob::dispatch($channel);
|
||||||
|
|
||||||
$this->nextStep();
|
$this->nextStep();
|
||||||
} catch (RuntimeException $e) {
|
} catch (\Exception $e) {
|
||||||
$this->formErrors['general'] = $e->getMessage();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->formErrors['general'] = 'Failed to create channel. Please try again.';
|
$this->formErrors['general'] = 'Failed to create channel. Please try again.';
|
||||||
} finally {
|
} finally {
|
||||||
$this->isLoading = false;
|
$this->isLoading = false;
|
||||||
|
|
@ -287,17 +339,18 @@ public function createRoute(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->createRouteAction->execute(
|
Route::create([
|
||||||
$this->routeFeedId,
|
'feed_id' => $this->routeFeedId,
|
||||||
$this->routeChannelId,
|
'platform_channel_id' => $this->routeChannelId,
|
||||||
$this->routePriority,
|
'priority' => $this->routePriority,
|
||||||
);
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
// Trigger article discovery
|
// Trigger article discovery
|
||||||
ArticleDiscoveryJob::dispatch();
|
ArticleDiscoveryJob::dispatch();
|
||||||
|
|
||||||
$this->nextStep();
|
$this->nextStep();
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->formErrors['general'] = 'Failed to create route. Please try again.';
|
$this->formErrors['general'] = 'Failed to create route. Please try again.';
|
||||||
} finally {
|
} finally {
|
||||||
$this->isLoading = false;
|
$this->isLoading = false;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ public function casts(): array
|
||||||
|
|
||||||
public function isValid(): bool
|
public function isValid(): bool
|
||||||
{
|
{
|
||||||
|
// Article is valid if it passed validation and wasn't rejected
|
||||||
return $this->validated_at !== null && ! $this->isRejected();
|
return $this->validated_at !== null && ! $this->isRejected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
24
phpunit.xml
24
phpunit.xml
|
|
@ -18,17 +18,17 @@
|
||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing" force="true"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<server name="APP_ENV" value="testing" force="true"/>
|
<env name="APP_KEY" value="base64:5VABFQKtzx6flRFn7rQUQYI/G8xLnkUSYPVaYz2s/4M="/>
|
||||||
<server name="APP_MAINTENANCE_DRIVER" value="file" force="true"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<server name="BCRYPT_ROUNDS" value="4" force="true"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<server name="CACHE_STORE" value="array" force="true"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
<server name="DB_CONNECTION" value="sqlite" force="true"/>
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
<server name="DB_DATABASE" value=":memory:" force="true"/>
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
<server name="MAIL_MAILER" value="array" force="true"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<server name="PULSE_ENABLED" value="false" force="true"/>
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
<server name="QUEUE_CONNECTION" value="sync" force="true"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
<server name="SESSION_DRIVER" value="array" force="true"/>
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
<server name="TELESCOPE_ENABLED" value="false" force="true"/>
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@
|
||||||
// Onboarding
|
// Onboarding
|
||||||
Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status');
|
Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status');
|
||||||
Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options');
|
Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options');
|
||||||
|
Route::post('/onboarding/platform', [OnboardingController::class, 'createPlatform'])->name('api.onboarding.platform');
|
||||||
|
Route::post('/onboarding/feed', [OnboardingController::class, 'createFeed'])->name('api.onboarding.feed');
|
||||||
|
Route::post('/onboarding/channel', [OnboardingController::class, 'createChannel'])->name('api.onboarding.channel');
|
||||||
|
Route::post('/onboarding/route', [OnboardingController::class, 'createRoute'])->name('api.onboarding.route');
|
||||||
Route::post('/onboarding/complete', [OnboardingController::class, 'complete'])->name('api.onboarding.complete');
|
Route::post('/onboarding/complete', [OnboardingController::class, 'complete'])->name('api.onboarding.complete');
|
||||||
Route::post('/onboarding/skip', [OnboardingController::class, 'skip'])->name('api.onboarding.skip');
|
Route::post('/onboarding/skip', [OnboardingController::class, 'skip'])->name('api.onboarding.skip');
|
||||||
Route::post('/onboarding/reset-skip', [OnboardingController::class, 'resetSkip'])->name('api.onboarding.reset-skip');
|
Route::post('/onboarding/reset-skip', [OnboardingController::class, 'resetSkip'])->name('api.onboarding.reset-skip');
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ public function test_index_returns_feeds_ordered_by_active_status_then_name(): v
|
||||||
|
|
||||||
public function test_store_creates_vrt_feed_successfully(): void
|
public function test_store_creates_vrt_feed_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->english()->create();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'VRT Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
|
|
@ -81,7 +81,7 @@ public function test_store_creates_vrt_feed_successfully(): void
|
||||||
|
|
||||||
public function test_store_creates_belga_feed_successfully(): void
|
public function test_store_creates_belga_feed_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->english()->create();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
|
|
@ -99,7 +99,7 @@ public function test_store_creates_belga_feed_successfully(): void
|
||||||
'data' => [
|
'data' => [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
'type' => 'rss',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
@ -107,13 +107,13 @@ public function test_store_creates_belga_feed_successfully(): void
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
'type' => 'rss',
|
'type' => 'website',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_sets_default_active_status(): void
|
public function test_store_sets_default_active_status(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->english()->create();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use App\Services\Auth\LemmyAuthService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -188,6 +189,180 @@ public function test_options_returns_languages_and_platform_instances()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_create_feed_validates_required_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/feed', []);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['name', 'provider', 'language_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_feed_creates_vrt_feed_successfully()
|
||||||
|
{
|
||||||
|
$feedData = [
|
||||||
|
'name' => 'VRT Test Feed',
|
||||||
|
'provider' => 'vrt',
|
||||||
|
'language_id' => 1,
|
||||||
|
'description' => 'Test description',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'name' => 'VRT Test Feed',
|
||||||
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
|
'type' => 'website',
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
'name' => 'VRT Test Feed',
|
||||||
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
|
'type' => 'website',
|
||||||
|
'language_id' => 1,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_feed_creates_belga_feed_successfully()
|
||||||
|
{
|
||||||
|
$feedData = [
|
||||||
|
'name' => 'Belga Test Feed',
|
||||||
|
'provider' => 'belga',
|
||||||
|
'language_id' => 1,
|
||||||
|
'description' => 'Test description',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'name' => 'Belga Test Feed',
|
||||||
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
|
'type' => 'website',
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
'name' => 'Belga Test Feed',
|
||||||
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
|
'type' => 'website',
|
||||||
|
'language_id' => 1,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_feed_rejects_invalid_provider()
|
||||||
|
{
|
||||||
|
$feedData = [
|
||||||
|
'name' => 'Invalid Feed',
|
||||||
|
'provider' => 'invalid',
|
||||||
|
'language_id' => 1,
|
||||||
|
'description' => 'Test description',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['provider']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_validates_required_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/channel', []);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['name', 'platform_instance_id', 'language_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_creates_channel_successfully()
|
||||||
|
{
|
||||||
|
$platformInstance = PlatformInstance::factory()->create();
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
// Create a platform account for this instance first
|
||||||
|
PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $platformInstance->url,
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$channelData = [
|
||||||
|
'name' => 'test_community',
|
||||||
|
'platform_instance_id' => $platformInstance->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'description' => 'Test community description',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/channel', $channelData);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'name' => 'test_community',
|
||||||
|
'display_name' => 'Test_community',
|
||||||
|
'channel_id' => 'test_community',
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
|
'name' => 'test_community',
|
||||||
|
'channel_id' => 'test_community',
|
||||||
|
'platform_instance_id' => $platformInstance->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_route_validates_required_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/route', []);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['feed_id', 'platform_channel_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_route_creates_route_successfully()
|
||||||
|
{
|
||||||
|
$language = Language::first();
|
||||||
|
$feed = Feed::factory()->language($language)->create();
|
||||||
|
$platformChannel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$routeData = [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $platformChannel->id,
|
||||||
|
'priority' => 75,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/route', $routeData);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $platformChannel->id,
|
||||||
|
'priority' => 75,
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('routes', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $platformChannel->id,
|
||||||
|
'priority' => 75,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_complete_onboarding_returns_success()
|
public function test_complete_onboarding_returns_success()
|
||||||
{
|
{
|
||||||
$response = $this->postJson('/api/v1/onboarding/complete');
|
$response = $this->postJson('/api/v1/onboarding/complete');
|
||||||
|
|
@ -268,6 +443,27 @@ public function test_reset_skip_works_when_no_setting_exists()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_create_platform_validates_instance_url_format()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/platform', [
|
||||||
|
'instance_url' => 'invalid.domain.with.spaces and symbols!',
|
||||||
|
'username' => 'testuser',
|
||||||
|
'password' => 'password123',
|
||||||
|
'platform' => 'lemmy',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['instance_url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_platform_validates_required_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/v1/onboarding/platform', []);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['instance_url', 'username', 'password', 'platform']);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_onboarding_flow_integration()
|
public function test_onboarding_flow_integration()
|
||||||
{
|
{
|
||||||
// 1. Initial status - needs onboarding
|
// 1. Initial status - needs onboarding
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@
|
||||||
|
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use App\Services\Auth\LemmyAuthService;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Mockery;
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class PlatformAccountsControllerTest extends TestCase
|
class PlatformAccountsControllerTest extends TestCase
|
||||||
|
|
@ -44,21 +42,12 @@ public function test_index_returns_successful_response(): void
|
||||||
|
|
||||||
public function test_store_creates_platform_account_successfully(): void
|
public function test_store_creates_platform_account_successfully(): void
|
||||||
{
|
{
|
||||||
$mockAuth = Mockery::mock(LemmyAuthService::class);
|
|
||||||
$mockAuth->shouldReceive('authenticate')
|
|
||||||
->once()
|
|
||||||
->with('https://lemmy.example.com', 'testuser', 'testpass123')
|
|
||||||
->andReturn([
|
|
||||||
'jwt' => 'test-token',
|
|
||||||
'person_view' => ['person' => ['id' => 1, 'display_name' => null, 'bio' => null]],
|
|
||||||
]);
|
|
||||||
$this->app->instance(LemmyAuthService::class, $mockAuth);
|
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'platform' => 'lemmy',
|
'platform' => 'lemmy',
|
||||||
'instance_domain' => 'lemmy.example.com',
|
'instance_url' => 'https://lemmy.example.com',
|
||||||
'username' => 'testuser',
|
'username' => 'testuser',
|
||||||
'password' => 'testpass123',
|
'password' => 'testpass123',
|
||||||
|
'settings' => ['key' => 'value']
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/platform-accounts', $data);
|
$response = $this->postJson('/api/v1/platform-accounts', $data);
|
||||||
|
|
@ -94,7 +83,7 @@ public function test_store_validates_required_fields(): void
|
||||||
$response = $this->postJson('/api/v1/platform-accounts', []);
|
$response = $this->postJson('/api/v1/platform-accounts', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['platform', 'instance_domain', 'username', 'password']);
|
->assertJsonValidationErrors(['platform', 'instance_url', 'username', 'password']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_show_returns_platform_account_successfully(): void
|
public function test_show_returns_platform_account_successfully(): void
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,11 @@ public function test_store_creates_platform_channel_successfully(): void
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'test_channel',
|
'channel_id' => 'test_channel',
|
||||||
|
'name' => 'Test Channel',
|
||||||
|
'display_name' => 'Test Channel Display',
|
||||||
'description' => 'A test channel',
|
'description' => 'A test channel',
|
||||||
|
'is_active' => true
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/platform-channels', $data);
|
$response = $this->postJson('/api/v1/platform-channels', $data);
|
||||||
|
|
@ -86,7 +89,7 @@ public function test_store_creates_platform_channel_successfully(): void
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'channel_id' => 'test_channel',
|
'channel_id' => 'test_channel',
|
||||||
'name' => 'test_channel',
|
'name' => 'Test Channel',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,13 +98,14 @@ public function test_store_validates_required_fields(): void
|
||||||
$response = $this->postJson('/api/v1/platform-channels', []);
|
$response = $this->postJson('/api/v1/platform-channels', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['platform_instance_id', 'name']);
|
->assertJsonValidationErrors(['platform_instance_id', 'channel_id', 'name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_validates_platform_instance_exists(): void
|
public function test_store_validates_platform_instance_exists(): void
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
'platform_instance_id' => 999,
|
'platform_instance_id' => 999,
|
||||||
|
'channel_id' => 'test_channel',
|
||||||
'name' => 'Test Channel'
|
'name' => 'Test Channel'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
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\Services\Log\LogSaver;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use App\Services\Article\ArticleFetcher;
|
||||||
|
|
@ -176,10 +175,7 @@ public function test_exception_logged_event_is_dispatched(): void
|
||||||
|
|
||||||
public function test_validate_article_listener_processes_new_article(): void
|
public function test_validate_article_listener_processes_new_article(): void
|
||||||
{
|
{
|
||||||
Event::fake([ArticleApproved::class]);
|
Event::fake([ArticleReadyToPublish::class]);
|
||||||
|
|
||||||
// Disable approvals so listener auto-approves valid articles
|
|
||||||
Setting::setBool('enable_publishing_approvals', false);
|
|
||||||
|
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
|
|
@ -204,8 +200,8 @@ public function test_validate_article_listener_processes_new_article(): void
|
||||||
$listener->handle($event);
|
$listener->handle($event);
|
||||||
|
|
||||||
$article->refresh();
|
$article->refresh();
|
||||||
$this->assertEquals('approved', $article->approval_status);
|
$this->assertNotEquals('pending', $article->approval_status);
|
||||||
Event::assertDispatched(ArticleApproved::class);
|
$this->assertContains($article->approval_status, ['approved', 'rejected']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test removed - PublishApprovedArticle and ArticleReadyToPublish classes no longer exist
|
// Test removed - PublishApprovedArticle and ArticleReadyToPublish classes no longer exist
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\Actions;
|
|
||||||
|
|
||||||
use App\Actions\CreateChannelAction;
|
|
||||||
use App\Models\Language;
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class CreateChannelActionTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private CreateChannelAction $action;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->action = new CreateChannelAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_channel_and_attaches_account(): void
|
|
||||||
{
|
|
||||||
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
|
||||||
$account = PlatformAccount::factory()->create([
|
|
||||||
'instance_url' => 'https://lemmy.world',
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$channel = $this->action->execute('test_community', $instance->id, $language->id, 'A description');
|
|
||||||
|
|
||||||
$this->assertInstanceOf(PlatformChannel::class, $channel);
|
|
||||||
$this->assertEquals('test_community', $channel->name);
|
|
||||||
$this->assertEquals('test_community', $channel->channel_id);
|
|
||||||
$this->assertEquals('Test_community', $channel->display_name);
|
|
||||||
$this->assertEquals($instance->id, $channel->platform_instance_id);
|
|
||||||
$this->assertEquals($language->id, $channel->language_id);
|
|
||||||
$this->assertEquals('A description', $channel->description);
|
|
||||||
$this->assertTrue($channel->is_active);
|
|
||||||
|
|
||||||
// Verify account is attached
|
|
||||||
$this->assertTrue($channel->platformAccounts->contains($account));
|
|
||||||
$this->assertEquals(1, $channel->platformAccounts->first()->pivot->priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_channel_without_language(): void
|
|
||||||
{
|
|
||||||
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
|
||||||
PlatformAccount::factory()->create([
|
|
||||||
'instance_url' => 'https://lemmy.world',
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$channel = $this->action->execute('test_community', $instance->id);
|
|
||||||
|
|
||||||
$this->assertNull($channel->language_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_fails_when_no_active_accounts(): void
|
|
||||||
{
|
|
||||||
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
|
||||||
// Create inactive account
|
|
||||||
PlatformAccount::factory()->create([
|
|
||||||
'instance_url' => 'https://lemmy.world',
|
|
||||||
'is_active' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->expectException(\RuntimeException::class);
|
|
||||||
$this->expectExceptionMessage('No active platform accounts found for this instance');
|
|
||||||
|
|
||||||
$this->action->execute('test_community', $instance->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_fails_when_no_accounts_at_all(): void
|
|
||||||
{
|
|
||||||
$instance = PlatformInstance::factory()->create();
|
|
||||||
|
|
||||||
$this->expectException(\RuntimeException::class);
|
|
||||||
|
|
||||||
$this->action->execute('test_community', $instance->id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\Actions;
|
|
||||||
|
|
||||||
use App\Actions\CreateFeedAction;
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\Language;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class CreateFeedActionTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private CreateFeedAction $action;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->action = new CreateFeedAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_vrt_feed_with_correct_url(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create(['short_code' => 'en', 'is_active' => true]);
|
|
||||||
|
|
||||||
$feed = $this->action->execute('VRT News', 'vrt', $language->id, 'Test description');
|
|
||||||
|
|
||||||
$this->assertInstanceOf(Feed::class, $feed);
|
|
||||||
$this->assertEquals('VRT News', $feed->name);
|
|
||||||
$this->assertEquals('https://www.vrt.be/vrtnws/en/', $feed->url);
|
|
||||||
$this->assertEquals('website', $feed->type);
|
|
||||||
$this->assertEquals('vrt', $feed->provider);
|
|
||||||
$this->assertEquals($language->id, $feed->language_id);
|
|
||||||
$this->assertEquals('Test description', $feed->description);
|
|
||||||
$this->assertTrue($feed->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_belga_feed_with_correct_url(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create(['short_code' => 'en', 'is_active' => true]);
|
|
||||||
|
|
||||||
$feed = $this->action->execute('Belga News', 'belga', $language->id);
|
|
||||||
|
|
||||||
$this->assertEquals('https://www.belganewsagency.eu/', $feed->url);
|
|
||||||
$this->assertEquals('rss', $feed->type);
|
|
||||||
$this->assertEquals('belga', $feed->provider);
|
|
||||||
$this->assertNull($feed->description);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_vrt_feed_with_dutch_language(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create(['short_code' => 'nl', 'is_active' => true]);
|
|
||||||
|
|
||||||
$feed = $this->action->execute('VRT Nieuws', 'vrt', $language->id);
|
|
||||||
|
|
||||||
$this->assertEquals('https://www.vrt.be/vrtnws/nl/', $feed->url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_existing_feed_for_duplicate_url(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create(['short_code' => 'en', 'is_active' => true]);
|
|
||||||
|
|
||||||
$first = $this->action->execute('VRT News', 'vrt', $language->id);
|
|
||||||
$second = $this->action->execute('VRT News Duplicate', 'vrt', $language->id);
|
|
||||||
|
|
||||||
$this->assertEquals($first->id, $second->id);
|
|
||||||
$this->assertEquals(1, Feed::count());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_throws_exception_for_invalid_provider_language_combination(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create(['short_code' => 'fr', 'is_active' => true]);
|
|
||||||
|
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
|
||||||
|
|
||||||
$this->action->execute('Feed', 'belga', $language->id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\Actions;
|
|
||||||
|
|
||||||
use App\Actions\CreatePlatformAccountAction;
|
|
||||||
use App\Enums\PlatformEnum;
|
|
||||||
use App\Exceptions\PlatformAuthException;
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformInstance;
|
|
||||||
use App\Services\Auth\LemmyAuthService;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Mockery;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class CreatePlatformAccountActionTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private CreatePlatformAccountAction $action;
|
|
||||||
private LemmyAuthService $lemmyAuthService;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->lemmyAuthService = Mockery::mock(LemmyAuthService::class);
|
|
||||||
$this->action = new CreatePlatformAccountAction($this->lemmyAuthService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_platform_account_with_new_instance(): void
|
|
||||||
{
|
|
||||||
$this->lemmyAuthService
|
|
||||||
->shouldReceive('authenticate')
|
|
||||||
->once()
|
|
||||||
->with('https://lemmy.world', 'testuser', 'testpass')
|
|
||||||
->andReturn([
|
|
||||||
'jwt' => 'test-jwt-token',
|
|
||||||
'person_view' => [
|
|
||||||
'person' => [
|
|
||||||
'id' => 42,
|
|
||||||
'display_name' => 'Test User',
|
|
||||||
'bio' => 'A test bio',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$account = $this->action->execute('lemmy.world', 'testuser', 'testpass');
|
|
||||||
|
|
||||||
$this->assertInstanceOf(PlatformAccount::class, $account);
|
|
||||||
$this->assertEquals('testuser', $account->username);
|
|
||||||
$this->assertEquals('https://lemmy.world', $account->instance_url);
|
|
||||||
$this->assertEquals('lemmy', $account->platform->value);
|
|
||||||
$this->assertTrue($account->is_active);
|
|
||||||
$this->assertEquals('active', $account->status);
|
|
||||||
$this->assertEquals(42, $account->settings['person_id']);
|
|
||||||
$this->assertEquals('Test User', $account->settings['display_name']);
|
|
||||||
$this->assertEquals('A test bio', $account->settings['description']);
|
|
||||||
$this->assertEquals('test-jwt-token', $account->settings['api_token']);
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_instances', [
|
|
||||||
'url' => 'https://lemmy.world',
|
|
||||||
'platform' => 'lemmy',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reuses_existing_platform_instance(): void
|
|
||||||
{
|
|
||||||
$existingInstance = PlatformInstance::factory()->create([
|
|
||||||
'url' => 'https://lemmy.world',
|
|
||||||
'platform' => 'lemmy',
|
|
||||||
'name' => 'Existing Name',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->lemmyAuthService
|
|
||||||
->shouldReceive('authenticate')
|
|
||||||
->once()
|
|
||||||
->andReturn([
|
|
||||||
'jwt' => 'token',
|
|
||||||
'person_view' => ['person' => ['id' => 1, 'display_name' => null, 'bio' => null]],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$account = $this->action->execute('lemmy.world', 'user', 'pass');
|
|
||||||
|
|
||||||
$this->assertEquals($existingInstance->id, $account->settings['platform_instance_id']);
|
|
||||||
$this->assertEquals(1, PlatformInstance::where('url', 'https://lemmy.world')->count());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_propagates_auth_exception(): void
|
|
||||||
{
|
|
||||||
$this->lemmyAuthService
|
|
||||||
->shouldReceive('authenticate')
|
|
||||||
->once()
|
|
||||||
->andThrow(new PlatformAuthException(PlatformEnum::LEMMY, 'Invalid credentials'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->action->execute('lemmy.world', 'baduser', 'badpass');
|
|
||||||
$this->fail('Expected PlatformAuthException was not thrown');
|
|
||||||
} catch (PlatformAuthException) {
|
|
||||||
// No instance or account should be created on auth failure
|
|
||||||
$this->assertDatabaseCount('platform_instances', 0);
|
|
||||||
$this->assertDatabaseCount('platform_accounts', 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\Actions;
|
|
||||||
|
|
||||||
use App\Actions\CreateRouteAction;
|
|
||||||
use App\Models\Feed;
|
|
||||||
use App\Models\Language;
|
|
||||||
use App\Models\PlatformChannel;
|
|
||||||
use App\Models\Route;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class CreateRouteActionTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private CreateRouteAction $action;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->action = new CreateRouteAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_route_with_defaults(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
$feed = Feed::factory()->language($language)->create();
|
|
||||||
$channel = PlatformChannel::factory()->create();
|
|
||||||
|
|
||||||
$route = $this->action->execute($feed->id, $channel->id);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(Route::class, $route);
|
|
||||||
$this->assertEquals($feed->id, $route->feed_id);
|
|
||||||
$this->assertEquals($channel->id, $route->platform_channel_id);
|
|
||||||
$this->assertEquals(0, $route->priority);
|
|
||||||
$this->assertTrue($route->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_route_with_custom_priority(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
$feed = Feed::factory()->language($language)->create();
|
|
||||||
$channel = PlatformChannel::factory()->create();
|
|
||||||
|
|
||||||
$route = $this->action->execute($feed->id, $channel->id, 75);
|
|
||||||
|
|
||||||
$this->assertEquals(75, $route->priority);
|
|
||||||
$this->assertTrue($route->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_creates_inactive_route(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
$feed = Feed::factory()->language($language)->create();
|
|
||||||
$channel = PlatformChannel::factory()->create();
|
|
||||||
|
|
||||||
$route = $this->action->execute($feed->id, $channel->id, 0, false);
|
|
||||||
|
|
||||||
$this->assertFalse($route->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_returns_existing_route_for_duplicate_feed_channel_pair(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
$feed = Feed::factory()->language($language)->create();
|
|
||||||
$channel = PlatformChannel::factory()->create();
|
|
||||||
|
|
||||||
$first = $this->action->execute($feed->id, $channel->id, 10);
|
|
||||||
$second = $this->action->execute($feed->id, $channel->id, 99);
|
|
||||||
|
|
||||||
$this->assertEquals($first->id, $second->id);
|
|
||||||
$this->assertEquals(10, $second->priority);
|
|
||||||
$this->assertDatabaseCount('routes', 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -46,26 +46,15 @@ public function test_is_valid_returns_false_when_approval_status_is_rejected():
|
||||||
$this->assertFalse($article->isValid());
|
$this->assertFalse($article->isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_is_valid_returns_true_when_validated_and_not_rejected(): void
|
public function test_is_valid_returns_true_when_approval_status_is_approved(): void
|
||||||
{
|
{
|
||||||
$article = Article::factory()->make([
|
$article = Article::factory()->make([
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($article->isValid());
|
$this->assertTrue($article->isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_is_valid_returns_false_when_not_validated(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make([
|
|
||||||
'approval_status' => 'approved',
|
|
||||||
'validated_at' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertFalse($article->isValid());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_approved_returns_true_for_approved_status(): void
|
public function test_is_approved_returns_true_for_approved_status(): void
|
||||||
{
|
{
|
||||||
$article = Article::factory()->make(['approval_status' => 'approved']);
|
$article = Article::factory()->make(['approval_status' => 'approved']);
|
||||||
|
|
@ -160,12 +149,10 @@ public function test_can_be_published_requires_approval_when_approvals_enabled()
|
||||||
|
|
||||||
$pendingArticle = Article::factory()->make([
|
$pendingArticle = Article::factory()->make([
|
||||||
'approval_status' => 'pending',
|
'approval_status' => 'pending',
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$approvedArticle = Article::factory()->make([
|
$approvedArticle = Article::factory()->make([
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertFalse($pendingArticle->canBePublished());
|
$this->assertFalse($pendingArticle->canBePublished());
|
||||||
|
|
@ -178,8 +165,7 @@ public function test_can_be_published_returns_true_when_approvals_disabled(): vo
|
||||||
Setting::where('key', 'enable_publishing_approvals')->delete();
|
Setting::where('key', 'enable_publishing_approvals')->delete();
|
||||||
|
|
||||||
$article = Article::factory()->make([
|
$article = Article::factory()->make([
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved', // Only approved articles can be published
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($article->canBePublished());
|
$this->assertTrue($article->canBePublished());
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,7 @@ public function test_publish_to_routed_channels_returns_empty_collection_when_no
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved'
|
||||||
'validated_at' => now()
|
|
||||||
]);
|
]);
|
||||||
$extractedData = ['title' => 'Test Title'];
|
$extractedData = ['title' => 'Test Title'];
|
||||||
|
|
||||||
|
|
@ -80,7 +79,6 @@ public function test_publish_to_routed_channels_skips_routes_without_active_acco
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create a route with a channel but no active accounts
|
// Create a route with a channel but no active accounts
|
||||||
|
|
@ -107,8 +105,7 @@ public function test_publish_to_routed_channels_successfully_publishes_to_channe
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved',
|
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
|
||||||
'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
$platformInstance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
||||||
|
|
@ -154,8 +151,7 @@ public function test_publish_to_routed_channels_handles_publishing_failure_grace
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved',
|
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
|
||||||
'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
$platformInstance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
||||||
|
|
@ -196,8 +192,7 @@ public function test_publish_to_routed_channels_publishes_to_multiple_routes():
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved',
|
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
|
||||||
'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
$platformInstance = PlatformInstance::factory()->create();
|
||||||
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
||||||
|
|
@ -252,8 +247,7 @@ public function test_publish_to_routed_channels_filters_out_failed_publications(
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved',
|
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
|
||||||
'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
$platformInstance = PlatformInstance::factory()->create();
|
||||||
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
||||||
|
|
@ -311,7 +305,6 @@ public function test_publish_skips_duplicate_when_url_already_posted_to_channel(
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
'validated_at' => now(),
|
|
||||||
'url' => 'https://example.com/article-1',
|
'url' => 'https://example.com/article-1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -363,7 +356,6 @@ public function test_publish_skips_duplicate_when_title_already_posted_to_channe
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
'validated_at' => now(),
|
|
||||||
'url' => 'https://example.com/article-new-url',
|
'url' => 'https://example.com/article-new-url',
|
||||||
'title' => 'Breaking News: Something Happened',
|
'title' => 'Breaking News: Something Happened',
|
||||||
]);
|
]);
|
||||||
|
|
@ -416,7 +408,6 @@ public function test_publish_proceeds_when_no_duplicate_exists(): void
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
'approval_status' => 'approved',
|
||||||
'validated_at' => now(),
|
|
||||||
'url' => 'https://example.com/unique-article',
|
'url' => 'https://example.com/unique-article',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue