release/v1.4.2 #159
10 changed files with 424 additions and 25 deletions
35
app/Actions/BackfillRouteArticlesAction.php
Normal file
35
app/Actions/BackfillRouteArticlesAction.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Route;
|
||||||
|
|
||||||
|
class BackfillRouteArticlesAction
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CreateRouteArticlesAction $createRouteArticles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function execute(Route $route): void
|
||||||
|
{
|
||||||
|
if (! $route->is_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Articles already validated with content, but never routed to this
|
||||||
|
// route. Uses the stored content rather than re-fetching, so a large
|
||||||
|
// backlog cannot trigger a network storm (#157).
|
||||||
|
Article::query()
|
||||||
|
->where('feed_id', $route->feed_id)
|
||||||
|
->whereNotNull('content')
|
||||||
|
->whereDoesntHave('routeArticles', function ($query) use ($route) {
|
||||||
|
$query->where('feed_id', $route->feed_id)
|
||||||
|
->where('platform_channel_id', $route->platform_channel_id);
|
||||||
|
})
|
||||||
|
->lazy()
|
||||||
|
->each(function (Article $article) use ($route) {
|
||||||
|
$this->createRouteArticles->createForRoute($article, $route, (string) $article->content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Actions;
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Events\RouteActivated;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
|
|
||||||
class CreateRouteAction
|
class CreateRouteAction
|
||||||
|
|
@ -12,7 +13,7 @@ class CreateRouteAction
|
||||||
*/
|
*/
|
||||||
public function execute(int $feedId, int $platformChannelId, int $priority = 0, bool $isActive = true): Route
|
public function execute(int $feedId, int $platformChannelId, int $priority = 0, bool $isActive = true): Route
|
||||||
{
|
{
|
||||||
return Route::firstOrCreate(
|
$route = Route::firstOrCreate(
|
||||||
[
|
[
|
||||||
'feed_id' => $feedId,
|
'feed_id' => $feedId,
|
||||||
'platform_channel_id' => $platformChannelId,
|
'platform_channel_id' => $platformChannelId,
|
||||||
|
|
@ -22,5 +23,11 @@ public function execute(int $feedId, int $platformChannelId, int $priority = 0,
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($route->wasRecentlyCreated && $route->is_active) {
|
||||||
|
RouteActivated::dispatch($route->feed_id, $route->platform_channel_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,20 @@ public function execute(Article $article, string $content): void
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Batch-load all active keywords for this feed, grouped by channel
|
foreach ($activeRoutes as $route) {
|
||||||
$keywordsByChannel = Keyword::where('feed_id', $article->feed_id)
|
$this->createForRoute($article, $route, $content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createForRoute(Article $article, Route $route, string $content): void
|
||||||
|
{
|
||||||
|
$routeKeywords = Keyword::where('feed_id', $route->feed_id)
|
||||||
|
->where('platform_channel_id', $route->platform_channel_id)
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->get()
|
->get();
|
||||||
->groupBy('platform_channel_id');
|
|
||||||
|
|
||||||
// Match keywords against full article content, title, and description
|
// Match keywords against full article content, title, and description
|
||||||
$searchableContent = $content.' '.$article->title.' '.$article->description;
|
$searchableContent = $content.' '.$article->title.' '.$article->description;
|
||||||
|
|
||||||
foreach ($activeRoutes as $route) {
|
|
||||||
$routeKeywords = $keywordsByChannel->get($route->platform_channel_id, collect());
|
|
||||||
$status = $this->evaluateKeywords($routeKeywords, $searchableContent);
|
$status = $this->evaluateKeywords($routeKeywords, $searchableContent);
|
||||||
|
|
||||||
if ($status === ApprovalStatusEnum::PENDING && $this->shouldAutoApprove($route)) {
|
if ($status === ApprovalStatusEnum::PENDING && $this->shouldAutoApprove($route)) {
|
||||||
|
|
@ -48,7 +51,6 @@ public function execute(Article $article, string $content): void
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, Keyword> $keywords
|
* @param Collection<int, Keyword> $keywords
|
||||||
|
|
|
||||||
17
app/Events/RouteActivated.php
Normal file
17
app/Events/RouteActivated.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RouteActivated
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $feedId,
|
||||||
|
public int $platformChannelId,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
31
app/Listeners/BackfillRouteArticlesListener.php
Normal file
31
app/Listeners/BackfillRouteArticlesListener.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Actions\BackfillRouteArticlesAction;
|
||||||
|
use App\Events\RouteActivated;
|
||||||
|
use App\Models\Route;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
||||||
|
class BackfillRouteArticlesListener implements ShouldQueue
|
||||||
|
{
|
||||||
|
public string $queue = 'default';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private BackfillRouteArticlesAction $backfillRouteArticles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(RouteActivated $event): void
|
||||||
|
{
|
||||||
|
$route = Route::query()
|
||||||
|
->where('feed_id', $event->feedId)
|
||||||
|
->where('platform_channel_id', $event->platformChannelId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($route === null || ! $route->is_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->backfillRouteArticles->execute($route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Events\RouteActivated;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Keyword;
|
use App\Models\Keyword;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
|
|
@ -72,6 +73,8 @@ public function createRoute(): void
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
RouteActivated::dispatch($this->newFeedId, $this->newChannelId);
|
||||||
|
|
||||||
$this->closeCreateModal();
|
$this->closeCreateModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,6 +132,10 @@ public function toggle(int $feedId, int $channelId): void
|
||||||
|
|
||||||
$route->is_active = ! $route->is_active;
|
$route->is_active = ! $route->is_active;
|
||||||
$route->save();
|
$route->save();
|
||||||
|
|
||||||
|
if ($route->is_active) {
|
||||||
|
RouteActivated::dispatch($route->feed_id, $route->platform_channel_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(int $feedId, int $channelId): void
|
public function delete(int $feedId, int $channelId): void
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $title
|
* @property string $title
|
||||||
* @property string|null $description
|
* @property string|null $description
|
||||||
|
* @property string|null $content
|
||||||
* @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
|
||||||
|
|
|
||||||
143
tests/Feature/BackfillRouteArticlesTest.php
Normal file
143
tests/Feature/BackfillRouteArticlesTest.php
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Actions\BackfillRouteArticlesAction;
|
||||||
|
use App\Actions\CreateRouteAction;
|
||||||
|
use App\Actions\CreateRouteArticlesAction;
|
||||||
|
use App\Events\RouteActivated;
|
||||||
|
use App\Listeners\BackfillRouteArticlesListener;
|
||||||
|
use App\Livewire\Routes;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\RouteArticle;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class BackfillRouteArticlesTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function listener(): BackfillRouteArticlesListener
|
||||||
|
{
|
||||||
|
return new BackfillRouteArticlesListener(new BackfillRouteArticlesAction(new CreateRouteArticlesAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function strandedArticle(Route $route): Article
|
||||||
|
{
|
||||||
|
return Article::factory()->create([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'content' => 'Some article content',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeRoute(bool $isActive): Route
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
return Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'priority' => 50,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_backfills_stranded_articles(): void
|
||||||
|
{
|
||||||
|
$route = $this->makeRoute(true);
|
||||||
|
$article = $this->strandedArticle($route);
|
||||||
|
|
||||||
|
$this->listener()->handle(new RouteActivated($route->feed_id, $route->platform_channel_id));
|
||||||
|
|
||||||
|
$this->assertSame(1, RouteArticle::where('article_id', $article->id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_does_nothing_when_the_route_is_missing(): void
|
||||||
|
{
|
||||||
|
$this->listener()->handle(new RouteActivated(99999, 99999));
|
||||||
|
|
||||||
|
$this->assertSame(0, RouteArticle::count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_does_nothing_when_the_route_is_inactive(): void
|
||||||
|
{
|
||||||
|
$route = $this->makeRoute(false);
|
||||||
|
$this->strandedArticle($route);
|
||||||
|
|
||||||
|
$this->listener()->handle(new RouteActivated($route->feed_id, $route->platform_channel_id));
|
||||||
|
|
||||||
|
$this->assertSame(0, RouteArticle::count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_creating_a_route_backfills_articles_that_were_validated_with_no_route(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'content' => 'Some article content',
|
||||||
|
]);
|
||||||
|
|
||||||
|
(new CreateRouteAction)->execute($feed->id, $channel->id);
|
||||||
|
|
||||||
|
$this->assertSame(1, RouteArticle::where('article_id', $article->id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_toggling_a_route_inactive_then_active_backfills(): void
|
||||||
|
{
|
||||||
|
$route = $this->makeRoute(false);
|
||||||
|
$article = $this->strandedArticle($route);
|
||||||
|
|
||||||
|
Livewire::test(Routes::class)
|
||||||
|
->call('toggle', $route->feed_id, $route->platform_channel_id);
|
||||||
|
|
||||||
|
$this->assertTrue($route->fresh()->is_active);
|
||||||
|
$this->assertSame(1, RouteArticle::where('article_id', $article->id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_route_action_dispatches_route_activated(): void
|
||||||
|
{
|
||||||
|
Event::fake([RouteActivated::class]);
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
(new CreateRouteAction)->execute($feed->id, $channel->id);
|
||||||
|
|
||||||
|
Event::assertDispatched(
|
||||||
|
RouteActivated::class,
|
||||||
|
fn (RouteActivated $event) => $event->feedId === $feed->id && $event->platformChannelId === $channel->id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_route_action_does_not_dispatch_for_an_existing_route(): void
|
||||||
|
{
|
||||||
|
Event::fake([RouteActivated::class]);
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
(new CreateRouteAction)->execute($feed->id, $channel->id);
|
||||||
|
(new CreateRouteAction)->execute($feed->id, $channel->id);
|
||||||
|
|
||||||
|
Event::assertDispatchedTimes(RouteActivated::class, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_route_action_does_not_dispatch_for_an_inactive_route(): void
|
||||||
|
{
|
||||||
|
Event::fake([RouteActivated::class]);
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
(new CreateRouteAction)->execute($feed->id, $channel->id, 0, false);
|
||||||
|
|
||||||
|
Event::assertNotDispatched(RouteActivated::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Events\ActivityLogged;
|
use App\Events\ActivityLogged;
|
||||||
use App\Events\ExceptionOccurred;
|
use App\Events\ExceptionOccurred;
|
||||||
use App\Events\NewArticleFetched;
|
use App\Events\NewArticleFetched;
|
||||||
|
use App\Events\RouteActivated;
|
||||||
use App\Events\RouteArticleApproved;
|
use App\Events\RouteArticleApproved;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
@ -25,6 +26,7 @@ public static function eventProvider(): array
|
||||||
'ActivityLogged' => [ActivityLogged::class],
|
'ActivityLogged' => [ActivityLogged::class],
|
||||||
'ExceptionOccurred' => [ExceptionOccurred::class],
|
'ExceptionOccurred' => [ExceptionOccurred::class],
|
||||||
'NewArticleFetched' => [NewArticleFetched::class],
|
'NewArticleFetched' => [NewArticleFetched::class],
|
||||||
|
'RouteActivated' => [RouteActivated::class],
|
||||||
'RouteArticleApproved' => [RouteArticleApproved::class],
|
'RouteArticleApproved' => [RouteArticleApproved::class],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
154
tests/Unit/Actions/BackfillRouteArticlesActionTest.php
Normal file
154
tests/Unit/Actions/BackfillRouteArticlesActionTest.php
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Actions;
|
||||||
|
|
||||||
|
use App\Actions\BackfillRouteArticlesAction;
|
||||||
|
use App\Actions\CreateRouteArticlesAction;
|
||||||
|
use App\Enums\ApprovalStatusEnum;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\Keyword;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\RouteArticle;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class BackfillRouteArticlesActionTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function action(): BackfillRouteArticlesAction
|
||||||
|
{
|
||||||
|
return new BackfillRouteArticlesAction(new CreateRouteArticlesAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function route(bool $isActive = true): Route
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
return Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'priority' => 50,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function strandedArticle(Route $route): Article
|
||||||
|
{
|
||||||
|
return Article::factory()->create([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'title' => 'A title',
|
||||||
|
'description' => 'A description',
|
||||||
|
'content' => 'Some article content',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_backfills_articles_missing_a_route_article(): void
|
||||||
|
{
|
||||||
|
$route = $this->route();
|
||||||
|
$article = $this->strandedArticle($route);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('route_articles', [
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'platform_channel_id' => $route->platform_channel_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_is_idempotent(): void
|
||||||
|
{
|
||||||
|
$route = $this->route();
|
||||||
|
$this->strandedArticle($route);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertSame(1, RouteArticle::count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_skips_articles_already_routed_to_this_route(): void
|
||||||
|
{
|
||||||
|
$route = $this->route();
|
||||||
|
$article = $this->strandedArticle($route);
|
||||||
|
|
||||||
|
RouteArticle::create([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'platform_channel_id' => $route->platform_channel_id,
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'approval_status' => ApprovalStatusEnum::APPROVED,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertSame(1, RouteArticle::count());
|
||||||
|
$this->assertSame(ApprovalStatusEnum::APPROVED, RouteArticle::first()->approval_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_skips_articles_without_stored_content(): void
|
||||||
|
{
|
||||||
|
$route = $this->route();
|
||||||
|
Article::factory()->create([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'content' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertSame(0, RouteArticle::count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_does_nothing_for_an_inactive_route(): void
|
||||||
|
{
|
||||||
|
$route = $this->route(isActive: false);
|
||||||
|
$this->strandedArticle($route);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertSame(0, RouteArticle::count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_does_not_touch_articles_in_other_feeds(): void
|
||||||
|
{
|
||||||
|
$route = $this->route();
|
||||||
|
$this->strandedArticle($route);
|
||||||
|
|
||||||
|
$otherFeed = Feed::factory()->create();
|
||||||
|
$otherArticle = Article::factory()->create([
|
||||||
|
'feed_id' => $otherFeed->id,
|
||||||
|
'content' => 'Other content',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertSame(0, RouteArticle::where('article_id', $otherArticle->id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_keyword_matching_uses_the_stored_content_without_refetching(): void
|
||||||
|
{
|
||||||
|
Setting::setBool('enable_publishing_approvals', true);
|
||||||
|
$route = $this->route();
|
||||||
|
Article::factory()->create([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'title' => 'A title',
|
||||||
|
'description' => 'A description',
|
||||||
|
'content' => 'news from Brussels today',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Keyword::create([
|
||||||
|
'feed_id' => $route->feed_id,
|
||||||
|
'platform_channel_id' => $route->platform_channel_id,
|
||||||
|
'keyword' => 'brussels',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->action()->execute($route);
|
||||||
|
|
||||||
|
$this->assertSame(ApprovalStatusEnum::PENDING, RouteArticle::first()->approval_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue