diff --git a/backend/app/Http/Controllers/Api/V1/AuthController.php b/backend/app/Http/Controllers/Api/V1/AuthController.php
deleted file mode 100644
index 8f336e0..0000000
--- a/backend/app/Http/Controllers/Api/V1/AuthController.php
+++ /dev/null
@@ -1,112 +0,0 @@
-validate([
- 'email' => 'required|email',
- 'password' => 'required',
- ]);
-
- $user = User::where('email', $request->email)->first();
-
- if (!$user || !Hash::check($request->password, $user->password)) {
- return $this->sendError('Invalid credentials', [], 401);
- }
-
- $token = $user->createToken('api-token')->plainTextToken;
-
- return $this->sendResponse([
- 'user' => [
- 'id' => $user->id,
- 'name' => $user->name,
- 'email' => $user->email,
- ],
- 'token' => $token,
- 'token_type' => 'Bearer',
- ], 'Login successful');
- } catch (ValidationException $e) {
- return $this->sendValidationError($e->errors());
- } catch (\Exception $e) {
- return $this->sendError('Login failed: ' . $e->getMessage(), [], 500);
- }
- }
-
- /**
- * Register a new user
- */
- public function register(Request $request): JsonResponse
- {
- try {
- $validated = $request->validate([
- 'name' => 'required|string|max:255',
- 'email' => 'required|string|email|max:255|unique:users',
- 'password' => 'required|string|min:8|confirmed',
- ]);
-
- $user = User::create([
- 'name' => $validated['name'],
- 'email' => $validated['email'],
- 'password' => Hash::make($validated['password']),
- ]);
-
- $token = $user->createToken('api-token')->plainTextToken;
-
- return $this->sendResponse([
- 'user' => [
- 'id' => $user->id,
- 'name' => $user->name,
- 'email' => $user->email,
- ],
- 'token' => $token,
- 'token_type' => 'Bearer',
- ], 'Registration successful', 201);
- } catch (ValidationException $e) {
- return $this->sendValidationError($e->errors());
- } catch (\Exception $e) {
- return $this->sendError('Registration failed: ' . $e->getMessage(), [], 500);
- }
- }
-
- /**
- * Logout user (revoke token)
- */
- public function logout(Request $request): JsonResponse
- {
- try {
- $request->user()->currentAccessToken()->delete();
-
- return $this->sendResponse(null, 'Logged out successfully');
- } catch (\Exception $e) {
- return $this->sendError('Logout failed: ' . $e->getMessage(), [], 500);
- }
- }
-
- /**
- * Get current authenticated user
- */
- public function me(Request $request): JsonResponse
- {
- return $this->sendResponse([
- 'user' => [
- 'id' => $request->user()->id,
- 'name' => $request->user()->name,
- 'email' => $request->user()->email,
- ],
- ], 'User retrieved successfully');
- }
-}
\ No newline at end of file
diff --git a/backend/app/Jobs/SyncChannelPostsJob.php b/backend/app/Jobs/SyncChannelPostsJob.php
index 5f40503..4707b83 100644
--- a/backend/app/Jobs/SyncChannelPostsJob.php
+++ b/backend/app/Jobs/SyncChannelPostsJob.php
@@ -29,8 +29,8 @@ public static function dispatchForAllActiveChannels(): void
{
PlatformChannel::with(['platformInstance', 'platformAccounts'])
->whereHas('platformInstance', fn ($query) => $query->where('platform', PlatformEnum::LEMMY))
- ->whereHas('platformAccounts', fn ($query) => $query->where('is_active', true))
- ->where('is_active', true)
+ ->whereHas('platformAccounts', fn ($query) => $query->where('platform_accounts.is_active', true))
+ ->where('platform_channels.is_active', true)
->get()
->each(function (PlatformChannel $channel) {
self::dispatch($channel);
diff --git a/backend/app/Modules/Lemmy/Services/LemmyPublisher.php b/backend/app/Modules/Lemmy/Services/LemmyPublisher.php
index 68a2651..5b153d0 100644
--- a/backend/app/Modules/Lemmy/Services/LemmyPublisher.php
+++ b/backend/app/Modules/Lemmy/Services/LemmyPublisher.php
@@ -28,7 +28,7 @@ public function __construct(PlatformAccount $account)
*/
public function publishToChannel(Article $article, array $extractedData, PlatformChannel $channel): array
{
- $token = LemmyAuthService::getToken($this->account);
+ $token = resolve(LemmyAuthService::class)->getToken($this->account);
// Use the language ID from extracted data (should be set during validation)
$languageId = $extractedData['language_id'] ?? null;
@@ -37,7 +37,7 @@ public function publishToChannel(Article $article, array $extractedData, Platfor
$token,
$extractedData['title'] ?? 'Untitled',
$extractedData['description'] ?? '',
- $channel->channel_id,
+ (int) $channel->channel_id,
$article->url,
$extractedData['thumbnail'] ?? null,
$languageId
diff --git a/backend/app/Services/Auth/LemmyAuthService.php b/backend/app/Services/Auth/LemmyAuthService.php
index 1ee58b2..65ed4c6 100644
--- a/backend/app/Services/Auth/LemmyAuthService.php
+++ b/backend/app/Services/Auth/LemmyAuthService.php
@@ -13,7 +13,7 @@ class LemmyAuthService
/**
* @throws PlatformAuthException
*/
- public static function getToken(PlatformAccount $account): string
+ public function getToken(PlatformAccount $account): string
{
$cacheKey = "lemmy_jwt_token_$account->id";
$cachedToken = Cache::get($cacheKey);
diff --git a/backend/app/Services/OnboardingRedirectService.php b/backend/app/Services/OnboardingRedirectService.php
deleted file mode 100644
index 6382fbc..0000000
--- a/backend/app/Services/OnboardingRedirectService.php
+++ /dev/null
@@ -1,20 +0,0 @@
-input('redirect_to');
-
- if ($redirectTo) {
- return redirect($redirectTo)->with('success', $successMessage);
- }
-
- return redirect()->route($defaultRoute)->with('success', $successMessage);
- }
-}
\ No newline at end of file
diff --git a/backend/app/Services/Parsers/VrtArticlePageParser.php b/backend/app/Services/Parsers/VrtArticlePageParser.php
index 323713d..36e152d 100644
--- a/backend/app/Services/Parsers/VrtArticlePageParser.php
+++ b/backend/app/Services/Parsers/VrtArticlePageParser.php
@@ -12,7 +12,7 @@ public static function extractTitle(string $html): ?string
}
// Try h1 tag
- if (preg_match('/
]*>([^<]+)<\/h1>/i', $html, $matches)) {
+ if (preg_match('/]*>(.*?)<\/h1>/is', $html, $matches)) {
return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8');
}
diff --git a/backend/app/Services/RoutingValidationService.php b/backend/app/Services/RoutingValidationService.php
index 7ddff2a..beb6520 100644
--- a/backend/app/Services/RoutingValidationService.php
+++ b/backend/app/Services/RoutingValidationService.php
@@ -24,7 +24,7 @@ public function validateLanguageCompatibility(Feed $feed, Collection $channels):
continue;
}
- if ($feed->language !== $channel->language) {
+ if ($feed->language->id !== $channel->language->id) {
throw new RoutingMismatchException($feed, $channel);
}
}
diff --git a/backend/routes/api.php b/backend/routes/api.php
index 4b3b990..b67b885 100644
--- a/backend/routes/api.php
+++ b/backend/routes/api.php
@@ -1,7 +1,6 @@
group(function () {
- // Public authentication routes
- Route::post('/auth/login', [AuthController::class, 'login'])->name('api.auth.login');
- Route::post('/auth/register', [AuthController::class, 'register'])->name('api.auth.register');
-
- // Protected authentication routes
- Route::middleware('auth:sanctum')->group(function () {
- Route::post('/auth/logout', [AuthController::class, 'logout'])->name('api.auth.logout');
- Route::get('/auth/me', [AuthController::class, 'me'])->name('api.auth.me');
- });
-
- // For demo purposes, making most endpoints public. In production, wrap in auth:sanctum middleware
+ // All endpoints are public for demo purposes
// Route::middleware('auth:sanctum')->group(function () {
// Dashboard stats
Route::get('/dashboard/stats', [DashboardController::class, 'stats'])->name('api.dashboard.stats');
diff --git a/backend/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php b/backend/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php
index e02d822..ea6e8d6 100644
--- a/backend/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php
+++ b/backend/tests/Feature/Http/Console/Commands/FetchNewArticlesCommandTest.php
@@ -15,58 +15,48 @@ class FetchNewArticlesCommandTest extends TestCase
public function test_command_runs_successfully_when_feeds_exist(): void
{
- // Arrange
+ Queue::fake();
+
Feed::factory()->create(['is_active' => true]);
- // Act & Assert
/** @var PendingCommand $exitCode */
$exitCode = $this->artisan('article:refresh');
$exitCode->assertSuccessful();
- // The command should complete without the "no feeds" message
$exitCode->assertExitCode(0);
}
public function test_command_does_not_dispatch_jobs_when_no_active_feeds_exist(): void
{
- // Arrange
Queue::fake();
- // No active feeds created
- // Act
/** @var PendingCommand $exitCode */
$exitCode = $this->artisan('article:refresh');
- // Assert
$exitCode->assertSuccessful();
Queue::assertNotPushed(ArticleDiscoveryJob::class);
}
public function test_command_does_not_dispatch_jobs_when_only_inactive_feeds_exist(): void
{
- // Arrange
Queue::fake();
+
Feed::factory()->create(['is_active' => false]);
- // Act
/** @var PendingCommand $exitCode */
$exitCode = $this->artisan('article:refresh');
- // Assert
$exitCode->assertSuccessful();
Queue::assertNotPushed(ArticleDiscoveryJob::class);
}
public function test_command_logs_when_no_feeds_available(): void
{
- // Arrange
Queue::fake();
- // Act
/** @var PendingCommand $exitCode */
$exitCode = $this->artisan('article:refresh');
- // Assert
$exitCode->assertSuccessful();
$exitCode->expectsOutput('No active feeds found. Article discovery skipped.');
}
diff --git a/backend/tests/Unit/Console/Commands/FetchNewArticlesCommandTest.php b/backend/tests/Unit/Console/Commands/FetchNewArticlesCommandTest.php
new file mode 100644
index 0000000..80c2a07
--- /dev/null
+++ b/backend/tests/Unit/Console/Commands/FetchNewArticlesCommandTest.php
@@ -0,0 +1,127 @@
+create([
+ 'key' => 'article_processing_enabled',
+ 'value' => 'false'
+ ]);
+
+ $this->artisan('article:refresh')
+ ->expectsOutput('Article processing is disabled. Article discovery skipped.')
+ ->assertExitCode(0);
+
+ Queue::assertNotPushed(ArticleDiscoveryJob::class);
+ }
+
+ public function test_command_skips_when_no_active_feeds(): void
+ {
+ Queue::fake();
+
+ // Enable article processing
+ Setting::factory()->create([
+ 'key' => 'article_processing_enabled',
+ 'value' => 'true'
+ ]);
+
+ // Ensure no active feeds exist
+ Feed::factory()->create(['is_active' => false]);
+
+ $this->artisan('article:refresh')
+ ->expectsOutput('No active feeds found. Article discovery skipped.')
+ ->assertExitCode(0);
+
+ Queue::assertNotPushed(ArticleDiscoveryJob::class);
+ }
+
+ public function test_command_dispatches_job_when_conditions_met(): void
+ {
+ Queue::fake();
+
+ // Enable article processing
+ Setting::factory()->create([
+ 'key' => 'article_processing_enabled',
+ 'value' => 'true'
+ ]);
+
+ // Create at least one active feed
+ Feed::factory()->create(['is_active' => true]);
+
+ $this->artisan('article:refresh')
+ ->assertExitCode(0);
+
+ Queue::assertPushed(ArticleDiscoveryJob::class);
+ }
+
+ public function test_command_has_correct_signature(): void
+ {
+ $command = new FetchNewArticlesCommand();
+
+ $this->assertEquals('article:refresh', $command->getName());
+ }
+
+ public function test_command_has_correct_description(): void
+ {
+ $command = new FetchNewArticlesCommand();
+
+ $this->assertEquals('Fetches latest articles', $command->getDescription());
+ }
+
+ public function test_command_with_multiple_active_feeds_still_dispatches_once(): void
+ {
+ Queue::fake();
+
+ // Enable article processing
+ Setting::factory()->create([
+ 'key' => 'article_processing_enabled',
+ 'value' => 'true'
+ ]);
+
+ // Create multiple active feeds
+ Feed::factory()->count(3)->create(['is_active' => true]);
+
+ $command = new FetchNewArticlesCommand();
+ $result = $command->handle();
+
+ $this->assertEquals(0, $result);
+ Queue::assertPushed(ArticleDiscoveryJob::class, 1);
+ }
+
+ public function test_command_ignores_inactive_feeds(): void
+ {
+ Queue::fake();
+
+ // Enable article processing
+ Setting::factory()->create([
+ 'key' => 'article_processing_enabled',
+ 'value' => 'true'
+ ]);
+
+ // Create mix of active and inactive feeds, but ensure at least one active
+ Feed::factory()->create(['is_active' => true]);
+ Feed::factory()->count(2)->create(['is_active' => false]);
+
+ $command = new FetchNewArticlesCommand();
+ $result = $command->handle();
+
+ $this->assertEquals(0, $result);
+ Queue::assertPushed(ArticleDiscoveryJob::class);
+ }
+}
\ No newline at end of file
diff --git a/backend/tests/Unit/Console/Commands/SyncChannelPostsCommandTest.php b/backend/tests/Unit/Console/Commands/SyncChannelPostsCommandTest.php
new file mode 100644
index 0000000..0138e80
--- /dev/null
+++ b/backend/tests/Unit/Console/Commands/SyncChannelPostsCommandTest.php
@@ -0,0 +1,145 @@
+create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'url' => 'https://lemmy.test'
+ ]);
+
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'is_active' => true
+ ]);
+
+ $channel = PlatformChannel::factory()->create([
+ 'platform_instance_id' => $instance->id,
+ 'is_active' => true
+ ]);
+
+ // Link the account to the channel
+ $account->channels()->attach($channel->id);
+ }
+
+ public function test_command_has_correct_signature(): void
+ {
+ $command = new SyncChannelPostsCommand();
+
+ $this->assertEquals('channel:sync', $command->getName());
+ }
+
+ public function test_command_has_correct_description(): void
+ {
+ $command = new SyncChannelPostsCommand();
+
+ $this->assertEquals('Manually sync channel posts for a platform', $command->getDescription());
+ }
+
+ public function test_command_syncs_lemmy_by_default(): void
+ {
+ $this->createTestChannelData();
+
+ $this->artisan('channel:sync')
+ ->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels')
+ ->assertExitCode(0);
+
+ Queue::assertPushed(SyncChannelPostsJob::class);
+ }
+
+ public function test_command_syncs_lemmy_when_explicitly_specified(): void
+ {
+ $this->createTestChannelData();
+
+ $this->artisan('channel:sync lemmy')
+ ->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels')
+ ->assertExitCode(0);
+
+ Queue::assertPushed(SyncChannelPostsJob::class);
+ }
+
+ public function test_command_fails_with_unsupported_platform(): void
+ {
+ $this->artisan('channel:sync twitter')
+ ->expectsOutput('Unsupported platform: twitter')
+ ->assertExitCode(1);
+
+ Queue::assertNotPushed(SyncChannelPostsJob::class);
+ }
+
+ public function test_command_fails_with_invalid_platform(): void
+ {
+ $this->artisan('channel:sync invalid')
+ ->expectsOutput('Unsupported platform: invalid')
+ ->assertExitCode(1);
+
+ Queue::assertNotPushed(SyncChannelPostsJob::class);
+ }
+
+ public function test_command_handles_empty_platform_argument(): void
+ {
+ $this->createTestChannelData();
+
+ // When no platform is provided, it defaults to 'lemmy' per the signature
+ $this->artisan('channel:sync')
+ ->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels')
+ ->assertExitCode(0);
+
+ Queue::assertPushed(SyncChannelPostsJob::class);
+ }
+
+ public function test_sync_lemmy_returns_success_code(): void
+ {
+ $this->createTestChannelData();
+
+ $this->artisan('channel:sync lemmy')
+ ->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels')
+ ->assertExitCode(0);
+
+ Queue::assertPushed(SyncChannelPostsJob::class);
+ }
+
+ public function test_command_signature_accepts_platform_argument(): void
+ {
+ $command = new SyncChannelPostsCommand();
+
+ // Check that the command definition includes the platform argument
+ $definition = $command->getDefinition();
+ $this->assertTrue($definition->hasArgument('platform'));
+
+ $platformArg = $definition->getArgument('platform');
+ $this->assertEquals('lemmy', $platformArg->getDefault());
+ }
+
+ public function test_private_sync_lemmy_method_calls_job(): void
+ {
+ $this->createTestChannelData();
+
+ $this->artisan('channel:sync lemmy')
+ ->expectsOutput('Successfully dispatched sync jobs for all active Lemmy channels')
+ ->assertExitCode(0);
+
+ Queue::assertPushed(SyncChannelPostsJob::class);
+ }
+}
\ No newline at end of file
diff --git a/backend/tests/Unit/Exceptions/PublishExceptionTest.php b/backend/tests/Unit/Exceptions/PublishExceptionTest.php
new file mode 100644
index 0000000..e2abed0
--- /dev/null
+++ b/backend/tests/Unit/Exceptions/PublishExceptionTest.php
@@ -0,0 +1,104 @@
+create(['id' => 123]);
+
+ $exception = new PublishException($article, PlatformEnum::LEMMY);
+
+ $this->assertSame($article, $exception->getArticle());
+ $this->assertEquals(123, $exception->getArticle()->id);
+ }
+
+ public function test_get_platform_returns_correct_platform(): void
+ {
+ $article = Article::factory()->create();
+
+ $exception = new PublishException($article, PlatformEnum::LEMMY);
+
+ $this->assertSame(PlatformEnum::LEMMY, $exception->getPlatform());
+ }
+
+ public function test_get_platform_returns_null_when_no_platform(): void
+ {
+ $article = Article::factory()->create();
+
+ $exception = new PublishException($article, null);
+
+ $this->assertNull($exception->getPlatform());
+ }
+
+ public function test_constructor_creates_message_with_article_id_and_platform(): void
+ {
+ $article = Article::factory()->create(['id' => 456]);
+
+ $exception = new PublishException($article, PlatformEnum::LEMMY);
+
+ $this->assertEquals('Failed to publish article #456 to lemmy', $exception->getMessage());
+ }
+
+ public function test_constructor_creates_message_with_article_id_only(): void
+ {
+ $article = Article::factory()->create(['id' => 789]);
+
+ $exception = new PublishException($article, null);
+
+ $this->assertEquals('Failed to publish article #789', $exception->getMessage());
+ }
+
+ public function test_constructor_includes_previous_exception_message(): void
+ {
+ $article = Article::factory()->create(['id' => 321]);
+ $previousException = new Exception('Original error message');
+
+ $exception = new PublishException($article, PlatformEnum::LEMMY, $previousException);
+
+ $this->assertEquals('Failed to publish article #321 to lemmy: Original error message', $exception->getMessage());
+ $this->assertSame($previousException, $exception->getPrevious());
+ }
+
+ public function test_constructor_includes_previous_exception_message_without_platform(): void
+ {
+ $article = Article::factory()->create(['id' => 654]);
+ $previousException = new Exception('Another error');
+
+ $exception = new PublishException($article, null, $previousException);
+
+ $this->assertEquals('Failed to publish article #654: Another error', $exception->getMessage());
+ $this->assertSame($previousException, $exception->getPrevious());
+ }
+
+ public function test_exception_properties_are_immutable(): void
+ {
+ $article = Article::factory()->create();
+
+ $exception = new PublishException($article, PlatformEnum::LEMMY);
+
+ // These methods should return the same instances every time
+ $this->assertSame($exception->getArticle(), $exception->getArticle());
+ $this->assertSame($exception->getPlatform(), $exception->getPlatform());
+ }
+
+ public function test_works_with_different_platform_enum_values(): void
+ {
+ $article = Article::factory()->create();
+
+ // Test with different platform values (if more are available)
+ $exception = new PublishException($article, PlatformEnum::LEMMY);
+ $this->assertSame(PlatformEnum::LEMMY, $exception->getPlatform());
+ $this->assertStringContainsString('lemmy', $exception->getMessage());
+ }
+}
\ No newline at end of file
diff --git a/backend/tests/Unit/Modules/Lemmy/LemmyRequestTest.php b/backend/tests/Unit/Modules/Lemmy/LemmyRequestTest.php
new file mode 100644
index 0000000..07aaff4
--- /dev/null
+++ b/backend/tests/Unit/Modules/Lemmy/LemmyRequestTest.php
@@ -0,0 +1,173 @@
+assertInstanceOf(LemmyRequest::class, $request);
+ }
+
+ public function test_constructor_sets_instance_without_token(): void
+ {
+ $instance = 'lemmy.test';
+
+ $request = new LemmyRequest($instance);
+
+ $this->assertInstanceOf(LemmyRequest::class, $request);
+ }
+
+ public function test_get_makes_request_without_token(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test');
+ $response = $request->get('test');
+
+ $this->assertInstanceOf(Response::class, $response);
+ $this->assertTrue($response->successful());
+
+ Http::assertSent(function ($request) {
+ return $request->url() === 'https://lemmy.test/api/v3/test' &&
+ !$request->hasHeader('Authorization');
+ });
+ }
+
+ public function test_get_makes_request_with_token(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test', 'test-token');
+ $response = $request->get('test');
+
+ $this->assertInstanceOf(Response::class, $response);
+ $this->assertTrue($response->successful());
+
+ Http::assertSent(function ($request) {
+ return $request->url() === 'https://lemmy.test/api/v3/test' &&
+ $request->hasHeader('Authorization', 'Bearer test-token');
+ });
+ }
+
+ public function test_get_includes_parameters(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test*' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test');
+ $params = ['param1' => 'value1', 'param2' => 'value2'];
+ $response = $request->get('test', $params);
+
+ $this->assertTrue($response->successful());
+
+ Http::assertSent(function ($request) use ($params) {
+ $query = parse_url($request->url(), PHP_URL_QUERY);
+ parse_str($query, $queryParams);
+ return $queryParams === $params;
+ });
+ }
+
+ public function test_post_makes_request_without_token(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test');
+ $response = $request->post('test');
+
+ $this->assertInstanceOf(Response::class, $response);
+ $this->assertTrue($response->successful());
+
+ Http::assertSent(function ($request) {
+ return $request->url() === 'https://lemmy.test/api/v3/test' &&
+ $request->method() === 'POST' &&
+ !$request->hasHeader('Authorization');
+ });
+ }
+
+ public function test_post_makes_request_with_token(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test', 'test-token');
+ $response = $request->post('test');
+
+ $this->assertTrue($response->successful());
+
+ Http::assertSent(function ($request) {
+ return $request->url() === 'https://lemmy.test/api/v3/test' &&
+ $request->method() === 'POST' &&
+ $request->hasHeader('Authorization', 'Bearer test-token');
+ });
+ }
+
+ public function test_post_includes_data(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test');
+ $data = ['field1' => 'value1', 'field2' => 'value2'];
+ $response = $request->post('test', $data);
+
+ $this->assertTrue($response->successful());
+
+ Http::assertSent(function ($request) use ($data) {
+ return $request->data() === $data;
+ });
+ }
+
+ public function test_with_token_returns_self_and_updates_token(): void
+ {
+ $request = new LemmyRequest('lemmy.test');
+ $result = $request->withToken('new-token');
+
+ $this->assertSame($request, $result);
+
+ // Verify token is used in subsequent requests
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request->get('test');
+
+ Http::assertSent(function ($request) {
+ return $request->hasHeader('Authorization', 'Bearer new-token');
+ });
+ }
+
+ public function test_requests_have_timeout(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/test' => Http::response(['success' => true], 200)
+ ]);
+
+ $request = new LemmyRequest('lemmy.test');
+ $request->get('test');
+
+ // This tests that timeout is set - actual timeout value is implementation detail
+ Http::assertSent(function ($request) {
+ return str_contains($request->url(), 'lemmy.test');
+ });
+ }
+}
\ No newline at end of file
diff --git a/backend/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php b/backend/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php
new file mode 100644
index 0000000..fe74ec9
--- /dev/null
+++ b/backend/tests/Unit/Modules/Lemmy/Services/LemmyApiServiceTest.php
@@ -0,0 +1,417 @@
+assertInstanceOf(LemmyApiService::class, $service);
+ }
+
+ public function test_login_successful(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/user/login' => Http::response([
+ 'jwt' => 'test-jwt-token'
+ ], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->login('testuser', 'testpass');
+
+ $this->assertEquals('test-jwt-token', $result);
+
+ Http::assertSent(function ($request) {
+ return $request->url() === 'https://lemmy.test/api/v3/user/login' &&
+ $request->method() === 'POST' &&
+ $request->data() === [
+ 'username_or_email' => 'testuser',
+ 'password' => 'testpass'
+ ];
+ });
+ }
+
+ public function test_login_failed_response(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/user/login' => Http::response(['error' => 'Invalid credentials'], 401)
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Lemmy login failed', Mockery::type('array'));
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->login('testuser', 'wrongpass');
+
+ $this->assertNull($result);
+ }
+
+ public function test_login_missing_jwt_in_response(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/user/login' => Http::response(['success' => true], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->login('testuser', 'testpass');
+
+ $this->assertNull($result);
+ }
+
+ public function test_login_exception_handling(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/user/login' => function () {
+ throw new Exception('Network error');
+ }
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Lemmy login exception', ['error' => 'Network error']);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->login('testuser', 'testpass');
+
+ $this->assertNull($result);
+ }
+
+ public function test_get_community_id_successful(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/community*' => Http::response([
+ 'community_view' => [
+ 'community' => [
+ 'id' => 123
+ ]
+ ]
+ ], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->getCommunityId('testcommunity', 'test-token');
+
+ $this->assertEquals(123, $result);
+
+ Http::assertSent(function ($request) {
+ return str_contains($request->url(), 'community') &&
+ $request->hasHeader('Authorization', 'Bearer test-token');
+ });
+ }
+
+ public function test_get_community_id_failed_response(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/community*' => Http::response(['error' => 'Not found'], 404)
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Community lookup failed', Mockery::type('array'));
+
+ $service = new LemmyApiService('lemmy.test');
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Failed to fetch community: 404');
+
+ $service->getCommunityId('nonexistent', 'test-token');
+ }
+
+ public function test_get_community_id_missing_data(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/community*' => Http::response([
+ 'community_view' => ['community' => []]
+ ], 200)
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Community lookup failed', Mockery::type('array'));
+
+ $service = new LemmyApiService('lemmy.test');
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Community not found');
+
+ $service->getCommunityId('testcommunity', 'test-token');
+ }
+
+ public function test_sync_channel_posts_successful(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post/list*' => Http::response([
+ 'posts' => [
+ [
+ 'post' => [
+ 'id' => 1,
+ 'name' => 'Test Post 1',
+ 'url' => 'https://example.com/1',
+ 'published' => '2023-01-01T00:00:00Z'
+ ]
+ ],
+ [
+ 'post' => [
+ 'id' => 2,
+ 'name' => 'Test Post 2',
+ 'published' => '2023-01-02T00:00:00Z'
+ ]
+ ]
+ ]
+ ], 200)
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Synced channel posts', [
+ 'platform_channel_id' => 123,
+ 'posts_count' => 2
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $service->syncChannelPosts('test-token', 123, 'testcommunity');
+
+ // Verify posts were stored
+ $this->assertDatabaseCount('platform_channel_posts', 2);
+ $this->assertDatabaseHas('platform_channel_posts', [
+ 'platform' => PlatformEnum::LEMMY,
+ 'channel_id' => '123',
+ 'post_id' => '1'
+ ]);
+ }
+
+ public function test_sync_channel_posts_failed_response(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post/list*' => Http::response(['error' => 'Unauthorized'], 401)
+ ]);
+
+ Log::shouldReceive('warning')
+ ->once()
+ ->with('Failed to sync channel posts', [
+ 'status' => 401,
+ 'platform_channel_id' => 123
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $service->syncChannelPosts('test-token', 123, 'testcommunity');
+
+ // Verify no posts were stored
+ $this->assertDatabaseCount('platform_channel_posts', 0);
+ }
+
+ public function test_sync_channel_posts_exception_handling(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post/list*' => function () {
+ throw new Exception('Network error');
+ }
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Exception while syncing channel posts', [
+ 'error' => 'Network error',
+ 'platform_channel_id' => 123
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $service->syncChannelPosts('test-token', 123, 'testcommunity');
+
+ $this->assertDatabaseCount('platform_channel_posts', 0);
+ }
+
+ public function test_create_post_successful_minimal_data(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post' => Http::response([
+ 'post_view' => [
+ 'post' => [
+ 'id' => 456,
+ 'name' => 'Test Title'
+ ]
+ ]
+ ], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->createPost('test-token', 'Test Title', 'Test Body', 123);
+
+ $this->assertIsArray($result);
+ $this->assertEquals(456, $result['post_view']['post']['id']);
+
+ Http::assertSent(function ($request) {
+ return $request->url() === 'https://lemmy.test/api/v3/post' &&
+ $request->method() === 'POST' &&
+ $request->data() === [
+ 'name' => 'Test Title',
+ 'body' => 'Test Body',
+ 'community_id' => 123
+ ];
+ });
+ }
+
+ public function test_create_post_successful_with_all_optional_data(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post' => Http::response([
+ 'post_view' => ['post' => ['id' => 456]]
+ ], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->createPost(
+ 'test-token',
+ 'Test Title',
+ 'Test Body',
+ 123,
+ 'https://example.com',
+ 'https://example.com/thumb.jpg',
+ 2
+ );
+
+ $this->assertIsArray($result);
+
+ Http::assertSent(function ($request) {
+ return $request->data() === [
+ 'name' => 'Test Title',
+ 'body' => 'Test Body',
+ 'community_id' => 123,
+ 'url' => 'https://example.com',
+ 'custom_thumbnail' => 'https://example.com/thumb.jpg',
+ 'language_id' => 2
+ ];
+ });
+ }
+
+ public function test_create_post_failed_response(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post' => Http::response(['error' => 'Validation failed'], 400)
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Post creation failed', Mockery::type('array'));
+
+ $service = new LemmyApiService('lemmy.test');
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Failed to create post: 400');
+
+ $service->createPost('test-token', 'Test Title', 'Test Body', 123);
+ }
+
+ public function test_create_post_exception_handling(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/post' => function () {
+ throw new Exception('Network error');
+ }
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Post creation failed', ['error' => 'Network error']);
+
+ $service = new LemmyApiService('lemmy.test');
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Network error');
+
+ $service->createPost('test-token', 'Test Title', 'Test Body', 123);
+ }
+
+ public function test_get_languages_successful(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/site' => Http::response([
+ 'all_languages' => [
+ ['id' => 0, 'code' => 'und', 'name' => 'Undetermined'],
+ ['id' => 1, 'code' => 'en', 'name' => 'English'],
+ ['id' => 2, 'code' => 'es', 'name' => 'Spanish']
+ ]
+ ], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->getLanguages();
+
+ $this->assertIsArray($result);
+ $this->assertCount(3, $result);
+ $this->assertEquals('English', $result[1]['name']);
+ }
+
+ public function test_get_languages_failed_response(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/site' => Http::response(['error' => 'Server error'], 500)
+ ]);
+
+ Log::shouldReceive('warning')
+ ->once()
+ ->with('Failed to fetch site languages', ['status' => 500]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->getLanguages();
+
+ $this->assertIsArray($result);
+ $this->assertEmpty($result);
+ }
+
+ public function test_get_languages_missing_data(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/site' => Http::response(['site_view' => []], 200)
+ ]);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->getLanguages();
+
+ $this->assertIsArray($result);
+ $this->assertEmpty($result);
+ }
+
+ public function test_get_languages_exception_handling(): void
+ {
+ Http::fake([
+ 'https://lemmy.test/api/v3/site' => function () {
+ throw new Exception('Network error');
+ }
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Exception while fetching languages', ['error' => 'Network error']);
+
+ $service = new LemmyApiService('lemmy.test');
+ $result = $service->getLanguages();
+
+ $this->assertIsArray($result);
+ $this->assertEmpty($result);
+ }
+}
\ No newline at end of file
diff --git a/backend/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php b/backend/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php
new file mode 100644
index 0000000..e02cd49
--- /dev/null
+++ b/backend/tests/Unit/Modules/Lemmy/Services/LemmyPublisherTest.php
@@ -0,0 +1,393 @@
+create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test'
+ ]);
+
+ $publisher = new LemmyPublisher($account);
+
+ $this->assertInstanceOf(LemmyPublisher::class, $publisher);
+ }
+
+ public function test_publish_to_channel_with_minimal_data(): void
+ {
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test',
+ 'username' => 'testuser',
+ 'password' => 'testpass'
+ ]);
+
+ $article = Article::factory()->create([
+ 'url' => 'https://example.com/article'
+ ]);
+
+ $instance = PlatformInstance::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'url' => 'https://lemmy.test'
+ ]);
+
+ $channel = PlatformChannel::factory()->create([
+ 'platform_instance_id' => $instance->id,
+ 'channel_id' => 123,
+ 'name' => 'testcommunity'
+ ]);
+
+ $extractedData = [
+ 'title' => 'Test Article',
+ 'description' => 'Test Description'
+ ];
+
+ $expectedResult = [
+ 'post_view' => [
+ 'post' => [
+ 'id' => 456,
+ 'name' => 'Test Article'
+ ]
+ ]
+ ];
+
+ // Mock LemmyAuthService
+ $this->mock(LemmyAuthService::class, function ($mock) use ($account) {
+ $mock->shouldReceive('getToken')
+ ->once()
+ ->with($account)
+ ->andReturn('test-jwt-token');
+ });
+
+ // Mock LemmyApiService
+ $apiServiceMock = Mockery::mock(LemmyApiService::class);
+ $apiServiceMock->shouldReceive('createPost')
+ ->once()
+ ->with(
+ 'test-jwt-token',
+ 'Test Article',
+ 'Test Description',
+ 123,
+ 'https://example.com/article',
+ null,
+ null
+ )
+ ->andReturn($expectedResult);
+
+ // Use reflection to replace the api service in the publisher
+ $publisher = new LemmyPublisher($account);
+ $reflection = new \ReflectionClass($publisher);
+ $apiProperty = $reflection->getProperty('api');
+ $apiProperty->setAccessible(true);
+ $apiProperty->setValue($publisher, $apiServiceMock);
+
+ $result = $publisher->publishToChannel($article, $extractedData, $channel);
+
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ public function test_publish_to_channel_with_all_optional_data(): void
+ {
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test',
+ 'username' => 'testuser',
+ 'password' => 'testpass'
+ ]);
+
+ $article = Article::factory()->create([
+ 'url' => 'https://example.com/article'
+ ]);
+
+ $instance = PlatformInstance::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'url' => 'https://lemmy.test'
+ ]);
+
+ $channel = PlatformChannel::factory()->create([
+ 'platform_instance_id' => $instance->id,
+ 'channel_id' => 123,
+ 'name' => 'testcommunity'
+ ]);
+
+ $extractedData = [
+ 'title' => 'Test Article',
+ 'description' => 'Test Description',
+ 'thumbnail' => 'https://example.com/thumb.jpg',
+ 'language_id' => 2
+ ];
+
+ $expectedResult = [
+ 'post_view' => [
+ 'post' => [
+ 'id' => 456,
+ 'name' => 'Test Article'
+ ]
+ ]
+ ];
+
+ // Mock LemmyAuthService
+ $this->mock(LemmyAuthService::class, function ($mock) use ($account) {
+ $mock->shouldReceive('getToken')
+ ->once()
+ ->with($account)
+ ->andReturn('test-jwt-token');
+ });
+
+ // Mock LemmyApiService
+ $apiServiceMock = Mockery::mock(LemmyApiService::class);
+ $apiServiceMock->shouldReceive('createPost')
+ ->once()
+ ->with(
+ 'test-jwt-token',
+ 'Test Article',
+ 'Test Description',
+ 123,
+ 'https://example.com/article',
+ 'https://example.com/thumb.jpg',
+ 2
+ )
+ ->andReturn($expectedResult);
+
+ // Use reflection to replace the api service in the publisher
+ $publisher = new LemmyPublisher($account);
+ $reflection = new \ReflectionClass($publisher);
+ $apiProperty = $reflection->getProperty('api');
+ $apiProperty->setAccessible(true);
+ $apiProperty->setValue($publisher, $apiServiceMock);
+
+ $result = $publisher->publishToChannel($article, $extractedData, $channel);
+
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ public function test_publish_to_channel_with_missing_title_uses_default(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test',
+ 'username' => 'testuser',
+ 'password' => 'testpass'
+ ]);
+
+ $article = Article::factory()->create([
+ 'url' => 'https://example.com/article'
+ ]);
+
+ $instance = PlatformInstance::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'url' => 'https://lemmy.test'
+ ]);
+
+ $channel = PlatformChannel::factory()->create([
+ 'platform_instance_id' => $instance->id,
+ 'channel_id' => 123,
+ 'name' => 'testcommunity'
+ ]);
+
+ $extractedData = [
+ 'description' => 'Test Description'
+ ];
+
+ // Mock LemmyAuthService
+ $this->mock(LemmyAuthService::class, function ($mock) use ($account) {
+ $mock->shouldReceive('getToken')
+ ->once()
+ ->with($account)
+ ->andReturn('test-jwt-token');
+ });
+
+ // Mock LemmyApiService
+ $apiServiceMock = Mockery::mock(LemmyApiService::class);
+ $apiServiceMock->shouldReceive('createPost')
+ ->once()
+ ->with(
+ 'test-jwt-token',
+ 'Untitled',
+ 'Test Description',
+ 123,
+ 'https://example.com/article',
+ null,
+ null
+ )
+ ->andReturn([]);
+
+ // Use reflection to replace the api service in the publisher
+ $publisher = new LemmyPublisher($account);
+ $reflection = new \ReflectionClass($publisher);
+ $apiProperty = $reflection->getProperty('api');
+ $apiProperty->setAccessible(true);
+ $apiProperty->setValue($publisher, $apiServiceMock);
+
+ $publisher->publishToChannel($article, $extractedData, $channel);
+ }
+
+ public function test_publish_to_channel_with_missing_description_uses_default(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test',
+ 'username' => 'testuser',
+ 'password' => 'testpass'
+ ]);
+
+ $article = Article::factory()->create([
+ 'url' => 'https://example.com/article'
+ ]);
+
+ $instance = PlatformInstance::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'url' => 'https://lemmy.test'
+ ]);
+
+ $channel = PlatformChannel::factory()->create([
+ 'platform_instance_id' => $instance->id,
+ 'channel_id' => 123,
+ 'name' => 'testcommunity'
+ ]);
+
+ $extractedData = [
+ 'title' => 'Test Title'
+ ];
+
+ // Mock LemmyAuthService
+ $this->mock(LemmyAuthService::class, function ($mock) use ($account) {
+ $mock->shouldReceive('getToken')
+ ->once()
+ ->with($account)
+ ->andReturn('test-jwt-token');
+ });
+
+ // Mock LemmyApiService
+ $apiServiceMock = Mockery::mock(LemmyApiService::class);
+ $apiServiceMock->shouldReceive('createPost')
+ ->once()
+ ->with(
+ 'test-jwt-token',
+ 'Test Title',
+ '',
+ 123,
+ 'https://example.com/article',
+ null,
+ null
+ )
+ ->andReturn([]);
+
+ // Use reflection to replace the api service in the publisher
+ $publisher = new LemmyPublisher($account);
+ $reflection = new \ReflectionClass($publisher);
+ $apiProperty = $reflection->getProperty('api');
+ $apiProperty->setAccessible(true);
+ $apiProperty->setValue($publisher, $apiServiceMock);
+
+ $publisher->publishToChannel($article, $extractedData, $channel);
+ }
+
+ public function test_publish_to_channel_throws_platform_auth_exception_when_auth_fails(): void
+ {
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test',
+ 'username' => 'testuser',
+ 'password' => 'testpass'
+ ]);
+
+ $article = Article::factory()->create();
+ $channel = PlatformChannel::factory()->create();
+ $extractedData = ['title' => 'Test', 'description' => 'Test'];
+
+ // Mock LemmyAuthService to throw exception
+ $this->mock(LemmyAuthService::class, function ($mock) use ($account) {
+ $mock->shouldReceive('getToken')
+ ->once()
+ ->with($account)
+ ->andThrow(new PlatformAuthException(PlatformEnum::LEMMY, 'Auth failed'));
+ });
+
+ $publisher = new LemmyPublisher($account);
+
+ $this->expectException(PlatformAuthException::class);
+ $this->expectExceptionMessage('Auth failed');
+
+ $publisher->publishToChannel($article, $extractedData, $channel);
+ }
+
+ public function test_publish_to_channel_throws_exception_when_create_post_fails(): void
+ {
+ $account = PlatformAccount::factory()->create([
+ 'platform' => PlatformEnum::LEMMY,
+ 'instance_url' => 'https://lemmy.test',
+ 'username' => 'testuser',
+ 'password' => 'testpass'
+ ]);
+
+ $article = Article::factory()->create();
+ $channel = PlatformChannel::factory()->create();
+ $extractedData = ['title' => 'Test', 'description' => 'Test'];
+
+ // Mock LemmyAuthService
+ $this->mock(LemmyAuthService::class, function ($mock) use ($account) {
+ $mock->shouldReceive('getToken')
+ ->once()
+ ->with($account)
+ ->andReturn('test-jwt-token');
+ });
+
+ // Mock LemmyApiService to throw exception
+ $apiServiceMock = Mockery::mock(LemmyApiService::class);
+ $apiServiceMock->shouldReceive('createPost')
+ ->once()
+ ->andThrow(new Exception('Post creation failed'));
+
+ // Use reflection to replace the api service in the publisher
+ $publisher = new LemmyPublisher($account);
+ $reflection = new \ReflectionClass($publisher);
+ $apiProperty = $reflection->getProperty('api');
+ $apiProperty->setAccessible(true);
+ $apiProperty->setValue($publisher, $apiServiceMock);
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Post creation failed');
+
+ $publisher->publishToChannel($article, $extractedData, $channel);
+ }
+}
\ No newline at end of file
diff --git a/backend/tests/Unit/Services/ArticleFetcherTest.php b/backend/tests/Unit/Services/ArticleFetcherTest.php
index 77a63ad..d32528c 100644
--- a/backend/tests/Unit/Services/ArticleFetcherTest.php
+++ b/backend/tests/Unit/Services/ArticleFetcherTest.php
@@ -11,12 +11,21 @@
use App\Services\Log\LogSaver;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Http;
use Mockery;
class ArticleFetcherTest extends TestCase
{
use RefreshDatabase;
+ protected function setUp(): void
+ {
+ parent::setUp();
+ Http::fake([
+ '*' => Http::response('', 200)
+ ]);
+ }
+
public function test_get_articles_from_feed_returns_collection(): void
{
$feed = Feed::factory()->create([
diff --git a/backend/tests/Unit/Services/Auth/LemmyAuthServiceTest.php b/backend/tests/Unit/Services/Auth/LemmyAuthServiceTest.php
index a5691f8..903f697 100644
--- a/backend/tests/Unit/Services/Auth/LemmyAuthServiceTest.php
+++ b/backend/tests/Unit/Services/Auth/LemmyAuthServiceTest.php
@@ -38,7 +38,8 @@ public function test_get_token_returns_cached_token_when_available(): void
->with($cacheKey)
->andReturn($cachedToken);
- $result = LemmyAuthService::getToken($account);
+ $service = new LemmyAuthService();
+ $result = $service->getToken($account);
$this->assertEquals($cachedToken, $result);
}
@@ -68,7 +69,8 @@ public function test_get_token_throws_exception_when_username_missing(): void
$this->expectException(PlatformAuthException::class);
$this->expectExceptionMessage('Missing credentials for account: ');
- LemmyAuthService::getToken($account);
+ $service = new LemmyAuthService();
+ $service->getToken($account);
}
public function test_get_token_throws_exception_when_password_missing(): void
@@ -96,7 +98,8 @@ public function test_get_token_throws_exception_when_password_missing(): void
$this->expectException(PlatformAuthException::class);
$this->expectExceptionMessage('Missing credentials for account: testuser');
- LemmyAuthService::getToken($account);
+ $service = new LemmyAuthService();
+ $service->getToken($account);
}
public function test_get_token_throws_exception_when_instance_url_missing(): void
@@ -124,7 +127,8 @@ public function test_get_token_throws_exception_when_instance_url_missing(): voi
$this->expectException(PlatformAuthException::class);
$this->expectExceptionMessage('Missing credentials for account: testuser');
- LemmyAuthService::getToken($account);
+ $service = new LemmyAuthService();
+ $service->getToken($account);
}
public function test_get_token_successfully_authenticates_and_caches_token(): void
@@ -169,8 +173,9 @@ public function test_get_token_uses_account_specific_cache_key(): void
->with($cacheKey2)
->andReturn('token2');
- $result1 = LemmyAuthService::getToken($account1);
- $result2 = LemmyAuthService::getToken($account2);
+ $service = new LemmyAuthService();
+ $result1 = $service->getToken($account1);
+ $result2 = $service->getToken($account2);
$this->assertEquals('token1', $result1);
$this->assertEquals('token2', $result2);
@@ -199,7 +204,8 @@ public function test_platform_auth_exception_contains_correct_platform(): void
->andReturn(null);
try {
- LemmyAuthService::getToken($account);
+ $service = new LemmyAuthService();
+ $service->getToken($account);
$this->fail('Expected PlatformAuthException to be thrown');
} catch (PlatformAuthException $e) {
$this->assertEquals(PlatformEnum::LEMMY, $e->getPlatform());
diff --git a/backend/tests/Unit/Services/Parsers/VrtArticlePageParserTest.php b/backend/tests/Unit/Services/Parsers/VrtArticlePageParserTest.php
new file mode 100644
index 0000000..b6a5c12
--- /dev/null
+++ b/backend/tests/Unit/Services/Parsers/VrtArticlePageParserTest.php
@@ -0,0 +1,325 @@
+