Compare commits
No commits in common. "923a0736a89242e8d989c136b16704cbc1be2a54" and "7d745b036a89c02abb736fff383597b5e0f2a5d5" have entirely different histories.
923a0736a8
...
7d745b036a
5 changed files with 15 additions and 264 deletions
|
|
@ -7,13 +7,15 @@
|
||||||
class BelgaHomepageParserAdapter implements HomepageParserInterface
|
class BelgaHomepageParserAdapter implements HomepageParserInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Belga is English-only for now (#124). The unused $language argument keeps
|
* Must stay in sync with config('feed.providers.belga.languages.en.url').
|
||||||
* the constructor compatible with HomepageParserFactory, which passes one
|
* Nothing calls getHomepageUrl() at runtime — the fetcher uses $feed->url,
|
||||||
* positionally to every homepage parser.
|
* seeded from that config value — so drift here would go unnoticed.
|
||||||
*
|
|
||||||
* @phpstan-ignore constructor.unusedParameter
|
|
||||||
*/
|
*/
|
||||||
public function __construct(string $language = 'en') {}
|
private const API_URL = 'https://capi.belga.press/belgapress/api/public/pressreleases?offset=1&count=6&search=&start=&end=&newsroomId=70&language=EN';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private string $language = 'en',
|
||||||
|
) {}
|
||||||
|
|
||||||
public function canParse(string $url): bool
|
public function canParse(string $url): bool
|
||||||
{
|
{
|
||||||
|
|
@ -27,9 +29,7 @@ public function extractArticleUrls(string $html): array
|
||||||
|
|
||||||
public function getHomepageUrl(): string
|
public function getHomepageUrl(): string
|
||||||
{
|
{
|
||||||
$url = config('feed.providers.belga.languages.en.url');
|
return str_replace('language=EN', 'language='.strtoupper($this->language), self::API_URL);
|
||||||
|
|
||||||
return is_string($url) ? $url : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSourceName(): string
|
public function getSourceName(): string
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'languages' => [
|
'languages' => [
|
||||||
'en' => ['url' => 'https://capi.belga.press/belgapress/api/public/pressreleases?offset=1&count=50&search=&start=&end=&newsroomId=70&language=EN'],
|
'en' => ['url' => 'https://capi.belga.press/belgapress/api/public/pressreleases?offset=1&count=6&search=&start=&end=&newsroomId=70&language=EN'],
|
||||||
],
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => BelgaHomepageParserAdapter::class,
|
'homepage' => BelgaHomepageParserAdapter::class,
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Belga retired its public RSS feed, so the provider switched to website
|
|
||||||
* discovery against their public JSON API (see #115).
|
|
||||||
*
|
|
||||||
* System feeds are maintained by the platform, not the end user, so their
|
|
||||||
* stored url/type are updated through migrations rather than the UI.
|
|
||||||
*
|
|
||||||
* Keyed on `provider`, never on the previous url: environments are at
|
|
||||||
* different points in this feed's history (some on the dead RSS url, some
|
|
||||||
* already on an earlier API url), and a url-keyed update would silently
|
|
||||||
* match nothing on most of them.
|
|
||||||
*
|
|
||||||
* `feeds.url` is unique while `feeds.provider` is not, so a blanket update
|
|
||||||
* across several belga rows would collide. CreateFeedAction looks feeds up by
|
|
||||||
* url, so a config url change can leave a second belga row behind: adopt the
|
|
||||||
* one already on the target url and deactivate the superseded ones.
|
|
||||||
*
|
|
||||||
* Superseded rows are deactivated rather than deleted — routes.feed_id
|
|
||||||
* cascades on delete, so removing a feed would take its routing rules with it.
|
|
||||||
*/
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
$url = config('feed.providers.belga.languages.en.url');
|
|
||||||
|
|
||||||
if (! is_string($url) || $url === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$keepId = DB::table('feeds')->where('provider', 'belga')->where('url', $url)->min('id')
|
|
||||||
?? DB::table('feeds')->where('provider', 'belga')->min('id');
|
|
||||||
|
|
||||||
if ($keepId === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::table('feeds')
|
|
||||||
->where('provider', 'belga')
|
|
||||||
->where('id', '!=', $keepId)
|
|
||||||
->update([
|
|
||||||
'is_active' => false,
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::table('feeds')
|
|
||||||
->where('id', $keepId)
|
|
||||||
->update([
|
|
||||||
'type' => 'website',
|
|
||||||
'url' => $url,
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
// No rollback: the previous RSS endpoint 404s, so restoring it would
|
|
||||||
// only reinstate a feed that silently yields no articles.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use stdClass;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The migration has already run by the time RefreshDatabase hands over, so
|
|
||||||
* these tests seed a pre-migration state and invoke up() against it directly.
|
|
||||||
*/
|
|
||||||
class SyncBelgaFeedMigrationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private const DEAD_RSS_URL = 'https://www.belganewsagency.eu/feed';
|
|
||||||
|
|
||||||
private function runMigration(): void
|
|
||||||
{
|
|
||||||
$migration = require database_path('migrations/2024_01_01_000012_sync_belga_feed_to_website_discovery.php');
|
|
||||||
|
|
||||||
$migration->up();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findFeed(int $id): stdClass
|
|
||||||
{
|
|
||||||
$feed = DB::table('feeds')->find($id);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(stdClass::class, $feed);
|
|
||||||
|
|
||||||
return $feed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function targetUrl(): string
|
|
||||||
{
|
|
||||||
$url = config('feed.providers.belga.languages.en.url');
|
|
||||||
|
|
||||||
$this->assertIsString($url);
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function seedFeed(string $url, string $type, bool $isActive = true, string $provider = 'belga'): int
|
|
||||||
{
|
|
||||||
return DB::table('feeds')->insertGetId([
|
|
||||||
'name' => 'Belga',
|
|
||||||
'provider' => $provider,
|
|
||||||
'type' => $type,
|
|
||||||
'url' => $url,
|
|
||||||
'is_active' => $isActive,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_converts_a_feed_still_on_the_dead_rss_url(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
$id = $this->seedFeed(self::DEAD_RSS_URL, 'rss');
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
$feed = $this->findFeed($id);
|
|
||||||
|
|
||||||
$this->assertSame('website', $feed->type);
|
|
||||||
$this->assertSame($this->targetUrl(), $feed->url);
|
|
||||||
$this->assertTrue((bool) $feed->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_is_idempotent_when_the_feed_is_already_migrated(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
$id = $this->seedFeed($this->targetUrl(), 'website');
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
$this->assertSame(1, DB::table('feeds')->where('provider', 'belga')->count());
|
|
||||||
|
|
||||||
$feed = $this->findFeed($id);
|
|
||||||
$this->assertSame('website', $feed->type);
|
|
||||||
$this->assertSame($this->targetUrl(), $feed->url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_deactivates_superseded_rows_instead_of_colliding_on_the_unique_url(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
$staleId = $this->seedFeed(self::DEAD_RSS_URL, 'rss');
|
|
||||||
$currentId = $this->seedFeed($this->targetUrl(), 'website');
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
// Both rows survive: routes.feed_id cascades on delete, so a superseded
|
|
||||||
// feed is deactivated rather than removed.
|
|
||||||
$this->assertSame(2, DB::table('feeds')->where('provider', 'belga')->count());
|
|
||||||
|
|
||||||
$current = $this->findFeed($currentId);
|
|
||||||
$this->assertTrue((bool) $current->is_active);
|
|
||||||
$this->assertSame($this->targetUrl(), $current->url);
|
|
||||||
$this->assertSame('website', $current->type);
|
|
||||||
|
|
||||||
$stale = $this->findFeed($staleId);
|
|
||||||
$this->assertFalse((bool) $stale->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_adopts_the_oldest_row_when_none_is_on_the_target_url(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
$oldestId = $this->seedFeed(self::DEAD_RSS_URL, 'rss');
|
|
||||||
$newerId = $this->seedFeed('https://capi.belga.press/belgapress/api/public/pressreleases?count=6', 'website');
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
// Neither row matches config, so the migration falls back to the lowest
|
|
||||||
// id — the oldest row, which is the one routes are most likely tied to.
|
|
||||||
$oldest = $this->findFeed($oldestId);
|
|
||||||
$this->assertSame($this->targetUrl(), $oldest->url);
|
|
||||||
$this->assertSame('website', $oldest->type);
|
|
||||||
$this->assertTrue((bool) $oldest->is_active);
|
|
||||||
|
|
||||||
$this->assertFalse((bool) $this->findFeed($newerId)->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_leaves_other_providers_untouched(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
$vrtId = $this->seedFeed('https://www.vrt.be/vrtnws/nl/', 'website', true, 'vrt');
|
|
||||||
// Seed a belga row too, so the migration actually runs its updates
|
|
||||||
// rather than bailing out at the "no belga feed" guard.
|
|
||||||
$this->seedFeed(self::DEAD_RSS_URL, 'rss');
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
$vrt = $this->findFeed($vrtId);
|
|
||||||
|
|
||||||
$this->assertSame('https://www.vrt.be/vrtnws/nl/', $vrt->url);
|
|
||||||
$this->assertSame('website', $vrt->type);
|
|
||||||
$this->assertTrue((bool) $vrt->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_down_is_a_deliberate_no_op(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
$id = $this->seedFeed($this->targetUrl(), 'website');
|
|
||||||
|
|
||||||
$migration = require database_path('migrations/2024_01_01_000012_sync_belga_feed_to_website_discovery.php');
|
|
||||||
$migration->down();
|
|
||||||
|
|
||||||
// Rolling back must not restore the retired RSS url.
|
|
||||||
$feed = $this->findFeed($id);
|
|
||||||
$this->assertSame($this->targetUrl(), $feed->url);
|
|
||||||
$this->assertSame('website', $feed->type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_does_nothing_when_no_belga_feed_exists(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
$this->assertSame(0, DB::table('feeds')->count());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_does_nothing_when_the_configured_url_is_missing(): void
|
|
||||||
{
|
|
||||||
DB::table('feeds')->delete();
|
|
||||||
config(['feed.providers.belga.languages.en.url' => '']);
|
|
||||||
$id = $this->seedFeed(self::DEAD_RSS_URL, 'rss');
|
|
||||||
|
|
||||||
$this->runMigration();
|
|
||||||
|
|
||||||
$feed = $this->findFeed($id);
|
|
||||||
|
|
||||||
$this->assertSame('rss', $feed->type);
|
|
||||||
$this->assertSame(self::DEAD_RSS_URL, $feed->url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
use App\Services\Parsers\BelgaHomepageParser;
|
use App\Services\Parsers\BelgaHomepageParser;
|
||||||
use App\Services\Parsers\BelgaHomepageParserAdapter;
|
use App\Services\Parsers\BelgaHomepageParserAdapter;
|
||||||
use Tests\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class BelgaHomepageParserTest extends TestCase
|
class BelgaHomepageParserTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -98,14 +98,10 @@ public function test_adapter_delegates_and_reports_source(): void
|
||||||
$this->assertSame('Belga News Agency', $adapter->getSourceName());
|
$this->assertSame('Belga News Agency', $adapter->getSourceName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_adapter_url_comes_from_config(): void
|
public function test_adapter_uppercases_language_in_api_url(): void
|
||||||
{
|
{
|
||||||
config(['feed.providers.belga.languages.en.url' => 'https://example.test/pressreleases?count=7']);
|
$this->assertStringContainsString('language=EN', (new BelgaHomepageParserAdapter('en'))->getHomepageUrl());
|
||||||
|
$this->assertStringContainsString('language=NL', (new BelgaHomepageParserAdapter('nl'))->getHomepageUrl());
|
||||||
$this->assertSame(
|
|
||||||
'https://example.test/pressreleases?count=7',
|
|
||||||
(new BelgaHomepageParserAdapter('en'))->getHomepageUrl()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_adapter_api_url_pins_load_bearing_query_params(): void
|
public function test_adapter_api_url_pins_load_bearing_query_params(): void
|
||||||
|
|
@ -115,7 +111,6 @@ public function test_adapter_api_url_pins_load_bearing_query_params(): void
|
||||||
$this->assertStringStartsWith('https://capi.belga.press/belgapress/api/public/pressreleases?', $url);
|
$this->assertStringStartsWith('https://capi.belga.press/belgapress/api/public/pressreleases?', $url);
|
||||||
$this->assertStringContainsString('newsroomId=70', $url);
|
$this->assertStringContainsString('newsroomId=70', $url);
|
||||||
$this->assertStringContainsString('offset=1', $url);
|
$this->assertStringContainsString('offset=1', $url);
|
||||||
$this->assertStringContainsString('count=50', $url);
|
$this->assertStringContainsString('count=6', $url);
|
||||||
$this->assertStringContainsString('language=EN', $url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue