Release v1.3.0 #100
15 changed files with 100 additions and 356 deletions
|
|
@ -40,40 +40,6 @@ public function index(Request $request): JsonResponse
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Approve an article
|
|
||||||
*/
|
|
||||||
public function approve(Article $article): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$article->approve('manual');
|
|
||||||
|
|
||||||
return $this->sendResponse(
|
|
||||||
new ArticleResource($article->fresh(['feed', 'articlePublication'])),
|
|
||||||
'Article approved and queued for publishing.'
|
|
||||||
);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $this->sendError('Failed to approve article: '.$e->getMessage(), [], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reject an article
|
|
||||||
*/
|
|
||||||
public function reject(Article $article): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$article->reject('manual');
|
|
||||||
|
|
||||||
return $this->sendResponse(
|
|
||||||
new ArticleResource($article->fresh(['feed', 'articlePublication'])),
|
|
||||||
'Article rejected.'
|
|
||||||
);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $this->sendError('Failed to reject article: '.$e->getMessage(), [], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually refresh articles from all active feeds
|
* Manually refresh articles from all active feeds
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ public function toArray(Request $request): array
|
||||||
'url' => $this->url,
|
'url' => $this->url,
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'is_valid' => $this->is_valid,
|
|
||||||
'approval_status' => $this->approval_status,
|
|
||||||
'publish_status' => $this->publish_status,
|
'publish_status' => $this->publish_status,
|
||||||
'validated_at' => $this->validated_at?->toISOString(),
|
'validated_at' => $this->validated_at?->toISOString(),
|
||||||
'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null,
|
'is_published' => $this->relationLoaded('articlePublication') && $this->articlePublication !== null,
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,6 @@ public function handle(ArticleApproved $event): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if not approved (safety check)
|
|
||||||
if (! $article->isApproved()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$article->update(['publish_status' => 'publishing']);
|
$article->update(['publish_status' => 'publishing']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Events\ArticleApproved;
|
|
||||||
use App\Events\NewArticleFetched;
|
use App\Events\NewArticleFetched;
|
||||||
use Database\Factories\ArticleFactory;
|
use Database\Factories\ArticleFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -23,14 +22,11 @@
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $title
|
* @property string $title
|
||||||
* @property string|null $description
|
* @property string|null $description
|
||||||
* @property string $approval_status
|
|
||||||
* @property string $publish_status
|
* @property string $publish_status
|
||||||
* @property bool|null $is_valid
|
|
||||||
* @property Carbon|null $validated_at
|
* @property Carbon|null $validated_at
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
* @property ArticlePublication|null $articlePublication
|
* @property ArticlePublication|null $articlePublication
|
||||||
* @property \Illuminate\Support\HigherOrderCollectionProxy|mixed $routeArticles
|
|
||||||
*/
|
*/
|
||||||
class Article extends Model
|
class Article extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -46,7 +42,6 @@ class Article extends Model
|
||||||
'image_url',
|
'image_url',
|
||||||
'published_at',
|
'published_at',
|
||||||
'author',
|
'author',
|
||||||
'approval_status',
|
|
||||||
'validated_at',
|
'validated_at',
|
||||||
'publish_status',
|
'publish_status',
|
||||||
];
|
];
|
||||||
|
|
@ -57,7 +52,6 @@ class Article extends Model
|
||||||
public function casts(): array
|
public function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'approval_status' => 'string',
|
|
||||||
'publish_status' => 'string',
|
'publish_status' => 'string',
|
||||||
'published_at' => 'datetime',
|
'published_at' => 'datetime',
|
||||||
'validated_at' => 'datetime',
|
'validated_at' => 'datetime',
|
||||||
|
|
@ -66,58 +60,6 @@ public function casts(): array
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isValid(): bool
|
|
||||||
{
|
|
||||||
return $this->validated_at !== null && ! $this->isRejected();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isApproved(): bool
|
|
||||||
{
|
|
||||||
return $this->approval_status === 'approved';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isPending(): bool
|
|
||||||
{
|
|
||||||
return $this->approval_status === 'pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isRejected(): bool
|
|
||||||
{
|
|
||||||
return $this->approval_status === 'rejected';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function approve(?string $approvedBy = null): void
|
|
||||||
{
|
|
||||||
$this->update([
|
|
||||||
'approval_status' => 'approved',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Fire event to trigger publishing
|
|
||||||
event(new ArticleApproved($this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reject(?string $rejectedBy = null): void
|
|
||||||
{
|
|
||||||
$this->update([
|
|
||||||
'approval_status' => 'rejected',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canBePublished(): bool
|
|
||||||
{
|
|
||||||
if (! $this->isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If approval system is disabled, auto-approve valid articles
|
|
||||||
if (! \App\Models\Setting::isPublishingApprovalsEnabled()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If approval system is enabled, only approved articles can be published
|
|
||||||
return $this->isApproved();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getIsPublishedAttribute(): bool
|
public function getIsPublishedAttribute(): bool
|
||||||
{
|
{
|
||||||
return $this->articlePublication()->exists();
|
return $this->articlePublication()->exists();
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,6 @@ public function publishRouteArticle(RouteArticle $routeArticle, array $extracted
|
||||||
*/
|
*/
|
||||||
public function publishToRoutedChannels(Article $article, array $extractedData): Collection
|
public function publishToRoutedChannels(Article $article, array $extractedData): Collection
|
||||||
{
|
{
|
||||||
if (! $article->isValid()) {
|
|
||||||
throw new PublishException($article, PlatformEnum::LEMMY, new RuntimeException('CANNOT_PUBLISH_INVALID_ARTICLE'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$feed = $article->feed;
|
$feed = $article->feed;
|
||||||
|
|
||||||
$activeRoutes = Route::where('feed_id', $feed->id)
|
$activeRoutes = Route::where('feed_id', $feed->id)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ public function definition(): array
|
||||||
'image_url' => $this->faker->optional()->imageUrl(),
|
'image_url' => $this->faker->optional()->imageUrl(),
|
||||||
'published_at' => $this->faker->optional()->dateTimeBetween('-1 month', 'now'),
|
'published_at' => $this->faker->optional()->dateTimeBetween('-1 month', 'now'),
|
||||||
'author' => $this->faker->optional()->name(),
|
'author' => $this->faker->optional()->name(),
|
||||||
'approval_status' => $this->faker->randomElement(['pending', 'approved', 'rejected']),
|
|
||||||
'publish_status' => 'unpublished',
|
'publish_status' => 'unpublished',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Migrate existing article approval_status to route_articles
|
||||||
|
$validatedArticles = DB::table('articles')
|
||||||
|
->whereIn('approval_status', ['approved', 'rejected'])
|
||||||
|
->whereNotNull('validated_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($validatedArticles as $article) {
|
||||||
|
$routes = DB::table('routes')
|
||||||
|
->where('feed_id', $article->feed_id)
|
||||||
|
->where('is_active', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$exists = DB::table('route_articles')
|
||||||
|
->where('feed_id', $route->feed_id)
|
||||||
|
->where('platform_channel_id', $route->platform_channel_id)
|
||||||
|
->where('article_id', $article->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('route_articles')->insert([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'platform_channel_id' => $route->platform_channel_id,
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'approval_status' => $article->approval_status,
|
||||||
|
'validated_at' => $article->validated_at,
|
||||||
|
'created_at' => $article->created_at,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove approval_status column from articles
|
||||||
|
Schema::table('articles', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['published_at', 'approval_status']);
|
||||||
|
$table->dropColumn('approval_status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('articles', function (Blueprint $table) {
|
||||||
|
$table->enum('approval_status', ['pending', 'approved', 'rejected'])->default('pending')->after('feed_id');
|
||||||
|
$table->index(['published_at', 'approval_status']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -47,8 +47,6 @@
|
||||||
|
|
||||||
// Articles
|
// Articles
|
||||||
Route::get('/articles', [ArticlesController::class, 'index'])->name('api.articles.index');
|
Route::get('/articles', [ArticlesController::class, 'index'])->name('api.articles.index');
|
||||||
Route::post('/articles/{article}/approve', [ArticlesController::class, 'approve'])->name('api.articles.approve');
|
|
||||||
Route::post('/articles/{article}/reject', [ArticlesController::class, 'reject'])->name('api.articles.reject');
|
|
||||||
Route::post('/articles/refresh', [ArticlesController::class, 'refresh'])->name('api.articles.refresh');
|
Route::post('/articles/refresh', [ArticlesController::class, 'refresh'])->name('api.articles.refresh');
|
||||||
|
|
||||||
// Platform Accounts
|
// Platform Accounts
|
||||||
|
|
|
||||||
|
|
@ -134,14 +134,12 @@ public function test_article_model_creates_successfully(): void
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
'url' => 'https://example.com/article',
|
'url' => 'https://example.com/article',
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('articles', [
|
$this->assertDatabaseHas('articles', [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
'url' => 'https://example.com/article',
|
'url' => 'https://example.com/article',
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($feed->id, $article->feed->id);
|
$this->assertEquals($feed->id, $article->feed->id);
|
||||||
|
|
|
||||||
|
|
@ -102,60 +102,6 @@ public function test_index_orders_articles_by_created_at_desc(): void
|
||||||
$this->assertEquals('First Article', $articles[1]['title']);
|
$this->assertEquals('First Article', $articles[1]['title']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_approve_article_successfully(): void
|
|
||||||
{
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/articles/{$article->id}/approve");
|
|
||||||
|
|
||||||
$response->assertStatus(200)
|
|
||||||
->assertJson([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Article approved and queued for publishing.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$article->refresh();
|
|
||||||
$this->assertEquals('approved', $article->approval_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_approve_nonexistent_article_returns_404(): void
|
|
||||||
{
|
|
||||||
$response = $this->postJson('/api/v1/articles/999/approve');
|
|
||||||
|
|
||||||
$response->assertStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reject_article_successfully(): void
|
|
||||||
{
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->postJson("/api/v1/articles/{$article->id}/reject");
|
|
||||||
|
|
||||||
$response->assertStatus(200)
|
|
||||||
->assertJson([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Article rejected.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$article->refresh();
|
|
||||||
$this->assertEquals('rejected', $article->approval_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reject_nonexistent_article_returns_404(): void
|
|
||||||
{
|
|
||||||
$response = $this->postJson('/api/v1/articles/999/reject');
|
|
||||||
|
|
||||||
$response->assertStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_index_includes_settings(): void
|
public function test_index_includes_settings(): void
|
||||||
{
|
{
|
||||||
$response = $this->getJson('/api/v1/articles');
|
$response = $this->getJson('/api/v1/articles');
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,6 @@ public function test_validate_article_listener_processes_new_article(): void
|
||||||
|
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Mock ArticleFetcher to return valid article data
|
// Mock ArticleFetcher to return valid article data
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public function test_exception_during_publishing_creates_error_notification(): v
|
||||||
$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',
|
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ public function test_no_publications_created_creates_warning_notification(): voi
|
||||||
$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',
|
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ public function test_successful_publish_does_not_create_notification(): void
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
|
||||||
'title' => 'Test Article',
|
'title' => 'Test Article',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ public function test_listener_validates_article_and_creates_route_articles(): vo
|
||||||
|
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$listener = $this->createListenerWithMockedFetcher('Article about Belgium');
|
$listener = $this->createListenerWithMockedFetcher('Article about Belgium');
|
||||||
|
|
@ -91,7 +90,6 @@ public function test_listener_skips_articles_with_existing_publication(): void
|
||||||
|
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ArticlePublication::create([
|
ArticlePublication::create([
|
||||||
|
|
@ -120,7 +118,6 @@ public function test_listener_handles_validation_errors_gracefully(): void
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$listener->handle(new NewArticleFetched($article));
|
$listener->handle(new NewArticleFetched($article));
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
|
|
||||||
namespace Tests\Unit\Models;
|
namespace Tests\Unit\Models;
|
||||||
|
|
||||||
use App\Events\ArticleApproved;
|
|
||||||
use App\Events\NewArticleFetched;
|
use App\Events\NewArticleFetched;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
@ -20,169 +18,9 @@ protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Mock HTTP requests to prevent external calls
|
|
||||||
Http::fake([
|
Http::fake([
|
||||||
'*' => Http::response('', 500),
|
'*' => Http::response('', 500),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Don't fake events globally - let individual tests control this
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_valid_returns_false_when_approval_status_is_pending(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make([
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertFalse($article->isValid());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_valid_returns_false_when_approval_status_is_rejected(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make([
|
|
||||||
'approval_status' => 'rejected',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertFalse($article->isValid());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_valid_returns_true_when_validated_and_not_rejected(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make([
|
|
||||||
'approval_status' => 'approved',
|
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$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
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make(['approval_status' => 'approved']);
|
|
||||||
|
|
||||||
$this->assertTrue($article->isApproved());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_approved_returns_false_for_non_approved_status(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make(['approval_status' => 'pending']);
|
|
||||||
|
|
||||||
$this->assertFalse($article->isApproved());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_pending_returns_true_for_pending_status(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make(['approval_status' => 'pending']);
|
|
||||||
|
|
||||||
$this->assertTrue($article->isPending());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_rejected_returns_true_for_rejected_status(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make(['approval_status' => 'rejected']);
|
|
||||||
|
|
||||||
$this->assertTrue($article->isRejected());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_approve_updates_status_and_triggers_event(): void
|
|
||||||
{
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$article->approve('test_user');
|
|
||||||
|
|
||||||
$article->refresh();
|
|
||||||
$this->assertEquals('approved', $article->approval_status);
|
|
||||||
|
|
||||||
Event::assertDispatched(ArticleApproved::class, function ($event) use ($article) {
|
|
||||||
return $event->article->id === $article->id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_approve_without_approved_by_parameter(): void
|
|
||||||
{
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$article->approve();
|
|
||||||
|
|
||||||
$article->refresh();
|
|
||||||
$this->assertEquals('approved', $article->approval_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reject_updates_status(): void
|
|
||||||
{
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$article->reject('test_user');
|
|
||||||
|
|
||||||
$article->refresh();
|
|
||||||
$this->assertEquals('rejected', $article->approval_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_be_published_returns_false_for_invalid_article(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->make([
|
|
||||||
'approval_status' => 'rejected', // rejected = not valid
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertFalse($article->canBePublished());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_be_published_requires_approval_when_approvals_enabled(): void
|
|
||||||
{
|
|
||||||
// Create a setting that enables approvals
|
|
||||||
Setting::create(['key' => 'enable_publishing_approvals', 'value' => '1']);
|
|
||||||
|
|
||||||
$pendingArticle = Article::factory()->make([
|
|
||||||
'approval_status' => 'pending',
|
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$approvedArticle = Article::factory()->make([
|
|
||||||
'approval_status' => 'approved',
|
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertFalse($pendingArticle->canBePublished());
|
|
||||||
$this->assertTrue($approvedArticle->canBePublished());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_be_published_returns_true_when_approvals_disabled(): void
|
|
||||||
{
|
|
||||||
// Make sure approvals are disabled (default behavior)
|
|
||||||
Setting::where('key', 'enable_publishing_approvals')->delete();
|
|
||||||
|
|
||||||
$article = Article::factory()->make([
|
|
||||||
'approval_status' => 'approved',
|
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertTrue($article->canBePublished());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_feed_relationship(): void
|
public function test_feed_relationship(): void
|
||||||
|
|
@ -194,6 +32,14 @@ public function test_feed_relationship(): void
|
||||||
$this->assertEquals($feed->id, $article->feed->id);
|
$this->assertEquals($feed->id, $article->feed->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_route_articles_relationship(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
$this->assertCount(0, $article->routeArticles);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_dispatch_fetched_event_fires_new_article_fetched_event(): void
|
public function test_dispatch_fetched_event_fires_new_article_fetched_event(): void
|
||||||
{
|
{
|
||||||
Event::fake([NewArticleFetched::class]);
|
Event::fake([NewArticleFetched::class]);
|
||||||
|
|
@ -207,4 +53,23 @@ public function test_dispatch_fetched_event_fires_new_article_fetched_event(): v
|
||||||
return $event->article->id === $article->id;
|
return $event->article->id === $article->id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_is_published_attribute(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
$this->assertFalse($article->is_published);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validated_at_is_cast_to_datetime(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'validated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Illuminate\Support\Carbon::class, $article->validated_at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
namespace Tests\Unit\Services\Publishing;
|
namespace Tests\Unit\Services\Publishing;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use App\Enums\PlatformEnum;
|
||||||
use App\Exceptions\PublishException;
|
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
|
|
@ -45,23 +44,12 @@ protected function tearDown(): void
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_throws_exception_for_invalid_article(): void
|
|
||||||
{
|
|
||||||
$article = Article::factory()->create(['approval_status' => 'rejected']);
|
|
||||||
$extractedData = ['title' => 'Test Title'];
|
|
||||||
|
|
||||||
$this->expectException(PublishException::class);
|
|
||||||
$this->expectExceptionMessage('CANNOT_PUBLISH_INVALID_ARTICLE');
|
|
||||||
|
|
||||||
$this->service->publishToRoutedChannels($article, $extractedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_returns_empty_collection_when_no_active_routes(): void
|
public function test_publish_to_routed_channels_returns_empty_collection_when_no_active_routes(): void
|
||||||
{
|
{
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
|
||||||
'validated_at' => now(),
|
'validated_at' => now(),
|
||||||
]);
|
]);
|
||||||
$extractedData = ['title' => 'Test Title'];
|
$extractedData = ['title' => 'Test Title'];
|
||||||
|
|
@ -78,7 +66,7 @@ public function test_publish_to_routed_channels_skips_routes_without_active_acco
|
||||||
$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',
|
|
||||||
'validated_at' => now(),
|
'validated_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -106,8 +94,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, 'validated_at' => now()]);
|
||||||
'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]);
|
||||||
|
|
@ -153,8 +140,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, 'validated_at' => now()]);
|
||||||
'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]);
|
||||||
|
|
@ -195,8 +181,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, 'validated_at' => now()]);
|
||||||
'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]);
|
||||||
|
|
@ -251,8 +236,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, 'validated_at' => now()]);
|
||||||
'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]);
|
||||||
|
|
@ -309,7 +293,7 @@ public function test_publish_skips_duplicate_when_url_already_posted_to_channel(
|
||||||
$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',
|
|
||||||
'validated_at' => now(),
|
'validated_at' => now(),
|
||||||
'url' => 'https://example.com/article-1',
|
'url' => 'https://example.com/article-1',
|
||||||
]);
|
]);
|
||||||
|
|
@ -361,7 +345,7 @@ public function test_publish_skips_duplicate_when_title_already_posted_to_channe
|
||||||
$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',
|
|
||||||
'validated_at' => now(),
|
'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',
|
||||||
|
|
@ -414,7 +398,7 @@ public function test_publish_proceeds_when_no_duplicate_exists(): void
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'approval_status' => 'approved',
|
|
||||||
'validated_at' => now(),
|
'validated_at' => now(),
|
||||||
'url' => 'https://example.com/unique-article',
|
'url' => 'https://example.com/unique-article',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue