Release v1.1.0 #79
8 changed files with 475 additions and 0 deletions
42
app/Actions/CreateChannelAction.php
Normal file
42
app/Actions/CreateChannelAction.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\PlatformAccount;
|
||||
use App\Models\PlatformChannel;
|
||||
use App\Models\PlatformInstance;
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
$channel = PlatformChannel::create([
|
||||
'platform_instance_id' => $platformInstanceId,
|
||||
'channel_id' => $name,
|
||||
'name' => $name,
|
||||
'display_name' => ucfirst($name),
|
||||
'description' => $description,
|
||||
'language_id' => $languageId,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$channel->platformAccounts()->attach($activeAccounts->first()->id, [
|
||||
'is_active' => true,
|
||||
'priority' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $channel->load('platformAccounts');
|
||||
}
|
||||
}
|
||||
35
app/Actions/CreateFeedAction.php
Normal file
35
app/Actions/CreateFeedAction.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\Language;
|
||||
|
||||
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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
50
app/Actions/CreatePlatformAccountAction.php
Normal file
50
app/Actions/CreatePlatformAccountAction.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Exceptions\PlatformAuthException;
|
||||
use App\Models\PlatformAccount;
|
||||
use App\Models\PlatformInstance;
|
||||
use App\Services\Auth\LemmyAuthService;
|
||||
|
||||
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);
|
||||
|
||||
$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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
18
app/Actions/CreateRouteAction.php
Normal file
18
app/Actions/CreateRouteAction.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\Route;
|
||||
|
||||
class CreateRouteAction
|
||||
{
|
||||
public function execute(int $feedId, int $platformChannelId, int $priority = 0, bool $isActive = true): Route
|
||||
{
|
||||
return Route::create([
|
||||
'feed_id' => $feedId,
|
||||
'platform_channel_id' => $platformChannelId,
|
||||
'priority' => $priority,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
}
|
||||
}
|
||||
86
tests/Unit/Actions/CreateChannelActionTest.php
Normal file
86
tests/Unit/Actions/CreateChannelActionTest.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
79
tests/Unit/Actions/CreateFeedActionTest.php
Normal file
79
tests/Unit/Actions/CreateFeedActionTest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
103
tests/Unit/Actions/CreatePlatformAccountActionTest.php
Normal file
103
tests/Unit/Actions/CreatePlatformAccountActionTest.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Actions;
|
||||
|
||||
use App\Actions\CreatePlatformAccountAction;
|
||||
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(\App\Enums\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);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
tests/Unit/Actions/CreateRouteActionTest.php
Normal file
62
tests/Unit/Actions/CreateRouteActionTest.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue