diff --git a/app/Services/Parsers/BelgaHomepageParser.php b/app/Services/Parsers/BelgaHomepageParser.php index 90028f81..f5374c65 100644 --- a/app/Services/Parsers/BelgaHomepageParser.php +++ b/app/Services/Parsers/BelgaHomepageParser.php @@ -16,8 +16,8 @@ public static function extractArticleUrls(string $json): array return collect($decoded['data']) ->pluck('id') - ->filter(fn($id) => is_int($id) || (is_string($id) && ctype_digit($id))) - ->map(fn($id) => sprintf(self::ARTICLE_URL_TEMPLATE, $id)) + ->filter(fn ($id) => is_int($id) || (is_string($id) && ctype_digit($id))) + ->map(fn ($id) => sprintf(self::ARTICLE_URL_TEMPLATE, $id)) ->unique() ->values() ->toArray(); diff --git a/app/Services/Parsers/BelgaHomepageParserAdapter.php b/app/Services/Parsers/BelgaHomepageParserAdapter.php index 3726cc00..0adb6750 100644 --- a/app/Services/Parsers/BelgaHomepageParserAdapter.php +++ b/app/Services/Parsers/BelgaHomepageParserAdapter.php @@ -6,6 +6,11 @@ class BelgaHomepageParserAdapter implements HomepageParserInterface { + /** + * Must stay in sync with config('feed.providers.belga.languages.en.url'). + * Nothing calls getHomepageUrl() at runtime — the fetcher uses $feed->url, + * seeded from that config value — so drift here would go unnoticed. + */ 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( @@ -24,7 +29,7 @@ public function extractArticleUrls(string $html): array public function getHomepageUrl(): string { - return str_replace('language=EN', 'language=' . strtoupper($this->language), self::API_URL); + return str_replace('language=EN', 'language='.strtoupper($this->language), self::API_URL); } public function getSourceName(): string diff --git a/config/feed.php b/config/feed.php index 4215761c..0d82739e 100644 --- a/config/feed.php +++ b/config/feed.php @@ -2,6 +2,7 @@ use App\Services\Parsers\BelgaArticlePageParser; use App\Services\Parsers\BelgaArticleParser; +use App\Services\Parsers\BelgaHomepageParserAdapter; use App\Services\Parsers\GuardianArticlePageParser; use App\Services\Parsers\GuardianArticleParser; use App\Services\Parsers\VrtArticlePageParser; @@ -41,12 +42,13 @@ 'code' => 'belga', 'name' => 'Belga News Agency', 'description' => 'Belgian national news agency', - 'type' => 'rss', + 'type' => 'website', 'is_active' => true, 'languages' => [ - 'en' => ['url' => 'https://www.belganewsagency.eu/feed'], + 'en' => ['url' => 'https://capi.belga.press/belgapress/api/public/pressreleases?offset=1&count=6&search=&start=&end=&newsroomId=70&language=EN'], ], 'parsers' => [ + 'homepage' => BelgaHomepageParserAdapter::class, 'article' => BelgaArticleParser::class, 'article_page' => BelgaArticlePageParser::class, ], diff --git a/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php index 0acac2b4..5bee7c96 100644 --- a/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/FeedsControllerTest.php @@ -92,22 +92,24 @@ public function test_store_creates_belga_feed_successfully(): void $response = $this->postJson('/api/v1/feeds', $feedData); + $expectedUrl = config('feed.providers.belga.languages.en.url'); + $response->assertStatus(201) ->assertJson([ 'success' => true, 'message' => 'Feed created successfully!', 'data' => [ 'name' => 'Belga Test Feed', - 'url' => 'https://www.belganewsagency.eu/feed', - 'type' => 'rss', + 'url' => $expectedUrl, + 'type' => 'website', 'is_active' => true, ], ]); $this->assertDatabaseHas('feeds', [ 'name' => 'Belga Test Feed', - 'url' => 'https://www.belganewsagency.eu/feed', - 'type' => 'rss', + 'url' => $expectedUrl, + 'type' => 'website', ]); } diff --git a/tests/Unit/Actions/CreateFeedActionTest.php b/tests/Unit/Actions/CreateFeedActionTest.php index accb3f54..001b7472 100644 --- a/tests/Unit/Actions/CreateFeedActionTest.php +++ b/tests/Unit/Actions/CreateFeedActionTest.php @@ -42,8 +42,9 @@ public function test_creates_belga_feed_with_correct_url(): void $feed = $this->action->execute('Belga News', 'belga', $language->id); - $this->assertEquals('https://www.belganewsagency.eu/feed', $feed->url); - $this->assertEquals('rss', $feed->type); + $this->assertStringStartsWith('https://capi.belga.press/belgapress/api/public/pressreleases?', $feed->url); + $this->assertStringContainsString('newsroomId=70', $feed->url); + $this->assertEquals('website', $feed->type); $this->assertEquals('belga', $feed->provider); $this->assertNull($feed->description); } diff --git a/tests/Unit/Services/ArticleFetcherBelgaTest.php b/tests/Unit/Services/ArticleFetcherBelgaTest.php new file mode 100644 index 00000000..3b319a5b --- /dev/null +++ b/tests/Unit/Services/ArticleFetcherBelgaTest.php @@ -0,0 +1,96 @@ +create(['short_code' => 'en']); + + return Feed::factory()->create([ + 'type' => 'website', + 'provider' => 'belga', + 'language_id' => $language->id, + 'url' => config('feed.providers.belga.languages.en.url'), + ]); + } + + public function test_creates_articles_from_belga_api_response(): void + { + Http::fake(['*' => Http::response($this->apiResponse(), 200)]); + + $result = $this->createArticleFetcher()->getArticlesFromFeed($this->belgaFeed()); + + $this->assertCount(6, $result); + $this->assertDatabaseHas('articles', [ + 'url' => 'https://www.belganewsagency.eu/press-releases/35285/', + ]); + $this->assertDatabaseHas('articles', [ + 'url' => 'https://www.belganewsagency.eu/press-releases/35269/', + ]); + } + + public function test_associates_created_articles_with_the_feed(): void + { + Http::fake(['*' => Http::response($this->apiResponse(), 200)]); + + $feed = $this->belgaFeed(); + + $this->createArticleFetcher()->getArticlesFromFeed($feed); + + $this->assertDatabaseHas('articles', [ + 'url' => 'https://www.belganewsagency.eu/press-releases/35285/', + 'feed_id' => $feed->id, + ]); + } + + public function test_returns_empty_collection_when_api_returns_no_articles(): void + { + // A well-formed envelope with nothing in it is legitimate (quiet day), + // and must be a no-op rather than an error. + Http::fake(['*' => Http::response('{"data":[],"_meta":{"total":0}}', 200)]); + + $result = $this->createArticleFetcher()->getArticlesFromFeed($this->belgaFeed()); + + $this->assertEmpty($result); + $this->assertDatabaseCount('articles', 0); + } + + public function test_returns_empty_collection_when_api_returns_an_error_page(): void + { + // The failure mode that caused #115: a 404 HTML body reaching the parser. + Http::fake(['*' => Http::response('404 Not Found', 200)]); + + $result = $this->createArticleFetcher()->getArticlesFromFeed($this->belgaFeed()); + + $this->assertEmpty($result); + } + + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } +} diff --git a/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php b/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php index aeacb5a0..04be29cf 100644 --- a/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php +++ b/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php @@ -10,7 +10,7 @@ class BelgaHomepageParserTest extends TestCase { private function fixture(): string { - return file_get_contents(__DIR__ . '/../../../Fixtures/belga-pressreleases.json'); + return file_get_contents(__DIR__.'/../../../Fixtures/belga-pressreleases.json'); } public function test_extracts_article_urls_from_api_json(): void