From 2a4d4ad84da755dff5a1da46efc925948617842d Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 1 Aug 2026 13:43:50 +0200 Subject: [PATCH] 115 - Add Belga JSON discovery parser for press-release API --- .gitignore | 1 + app/Services/Parsers/BelgaHomepageParser.php | 25 +++++ .../Parsers/BelgaHomepageParserAdapter.php | 34 ++++++ tests/Fixtures/belga-pressreleases.json | 1 + .../Parsers/BelgaHomepageParserTest.php | 104 ++++++++++++++++++ 5 files changed, 165 insertions(+) create mode 100644 app/Services/Parsers/BelgaHomepageParser.php create mode 100644 app/Services/Parsers/BelgaHomepageParserAdapter.php create mode 100644 tests/Fixtures/belga-pressreleases.json create mode 100644 tests/Unit/Services/Parsers/BelgaHomepageParserTest.php diff --git a/.gitignore b/.gitignore index 58008dd4..a6e08b96 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ yarn-error.log /coverage-report* /coverage.xml /.php-cs-fixer.dist.php +/.php-cs-fixer.cache diff --git a/app/Services/Parsers/BelgaHomepageParser.php b/app/Services/Parsers/BelgaHomepageParser.php new file mode 100644 index 00000000..90028f81 --- /dev/null +++ b/app/Services/Parsers/BelgaHomepageParser.php @@ -0,0 +1,25 @@ +pluck('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 new file mode 100644 index 00000000..3726cc00 --- /dev/null +++ b/app/Services/Parsers/BelgaHomepageParserAdapter.php @@ -0,0 +1,34 @@ +language), self::API_URL); + } + + public function getSourceName(): string + { + return 'Belga News Agency'; + } +} diff --git a/tests/Fixtures/belga-pressreleases.json b/tests/Fixtures/belga-pressreleases.json new file mode 100644 index 00000000..4ebba6b9 --- /dev/null +++ b/tests/Fixtures/belga-pressreleases.json @@ -0,0 +1 @@ +{"data":[{"id":35285,"newsroom":{"id":70,"subSourceName":"Belga English","logoUrl":"https://3.ssl.belga.be/bpmedia:brand:205881:thumbnail?v=6a6c82d8&m=oiooaebg","profilePictureUrl":null,"active":true},"content":{"en":{"title":"FIFA cancels plans for World Cup commercialisation after fierce criticism","lead":"FIFA president Gianni Infantino has scrapped the controversial plan to sell shares of the FIFA World Cup to private investors. This was reported by British media, including Sky News and BBC, during the night from Friday to Saturday.","coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:181511382:full?v=6a6c82d8&m=cdodnjha","categories":[{"id":3,"name":"SPORTS"}]}},"coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:181511382:full?v=6a6c82d8&m=cdodnjha","publishDate":"2026-08-01T06:24:19"},{"id":35282,"newsroom":{"id":70,"subSourceName":"Belga English","logoUrl":"https://3.ssl.belga.be/bpmedia:brand:205881:thumbnail?v=6a6c82d8&m=oiooaebg","profilePictureUrl":null,"active":true},"content":{"en":{"title":"Excluding Spain from Schengen is \"in no way a solution\" for Ceuta, says Belgian FM Prévot","lead":"For minister of Foreign Affairs Maxime Prévot, a temporary exclusion of Spain from the Schengen Area is not a solution. He stated this on Friday in an additional response to the migration crisis in Ceuta, after more than 60,000 people crossed the border between Morocco and the Spanish exclave in recent days.","coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:182478454:full?v=6a6c82d8&m=anpkocdi","categories":[{"id":9,"name":"POLITICS"}]}},"coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:182478454:full?v=6a6c82d8&m=anpkocdi","publishDate":"2026-07-31T16:59:26"},{"id":35281,"newsroom":{"id":70,"subSourceName":"Belga English","logoUrl":"https://3.ssl.belga.be/bpmedia:brand:205881:thumbnail?v=6a6c82d8&m=oiooaebg","profilePictureUrl":null,"active":true},"content":{"en":{"title":"Belgium offers tankers and other vehicles for fire fighting to France","lead":"In the fight against the devastating forest fires that have destroyed thousands of hectares in France over the past few weeks, primarily in the Gironde region, Belgium has expanded its offer of assistance. Our country is ready to send water tankers should France request them, the office of minister of the Interior Bernard Quintin (MR) announced on Friday.","coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:182396536:full?v=6a6c82d8&m=fmbijool","categories":[{"id":10,"name":"GENERAL"}]}},"coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:182396536:full?v=6a6c82d8&m=fmbijool","publishDate":"2026-07-31T16:52:04"},{"id":35271,"newsroom":{"id":70,"subSourceName":"Belga English","logoUrl":"https://3.ssl.belga.be/bpmedia:brand:205881:thumbnail?v=6a6c82d8&m=oiooaebg","profilePictureUrl":null,"active":true},"content":{"en":{"title":"Brussels Region demands withdrawal of shared scooters and bikes by 1 September","lead":"All shared scooters and bikes in free-floating fleets must be removed from the Brussels streets by no later than 1 September. This was announced by Brussels Mobility on Thursday.","coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:32171547:full?v=6a6c82d8&m=gnegeckj","categories":[{"id":9,"name":"POLITICS"}]}},"coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:32171547:full?v=6a6c82d8&m=gnegeckj","publishDate":"2026-07-31T06:44:12"},{"id":35270,"newsroom":{"id":70,"subSourceName":"Belga English","logoUrl":"https://3.ssl.belga.be/bpmedia:brand:205881:thumbnail?v=6a6c82d8&m=oiooaebg","profilePictureUrl":null,"active":true},"content":{"en":{"title":"Canadian intern at SHAPE suspected of espionage for China","lead":"The Belgian authorities suspect a Canadian woman who was interning at NATO's military headquarters in Bergen (SHAPE) of working for China. Two anonymous sources familiar with the matter told Reuters.","coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:32171621:full?v=6a6c82d8&m=hkkippeg","categories":[{"id":10,"name":"GENERAL"}]}},"coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:32171621:full?v=6a6c82d8&m=hkkippeg","publishDate":"2026-07-31T06:37:12"},{"id":35269,"newsroom":{"id":70,"subSourceName":"Belga English","logoUrl":"https://3.ssl.belga.be/bpmedia:brand:205881:thumbnail?v=6a6c82d8&m=oiooaebg","profilePictureUrl":null,"active":true},"content":{"en":{"title":"Hamas confirms disarmament of group and Israeli withdrawal from Gaza","lead":"Representatives of Hamas have confirmed an agreement on the second phase of the ceasefire with Israel during the night from Thursday to Friday. This was reported to the French news agency AFP by two anonymous sources within Hamas following President Donald Trump's announcement of the agreement. The new phase includes, among other things, the disarmament of the extremist Palestinian organisation and the gradual withdrawal of the Israeli military from the Gaza Strip. The Israeli government has not yet responded to the agreement.","coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:182283582:full?v=6a6c82d8&m=pgklpfdh","categories":[{"id":10,"name":"GENERAL"}]}},"coverImageUrl":"https://picturepackcdn-h33aaywmsq-ew.a.run.app/belgapicturepack:182283582:full?v=6a6c82d8&m=pgklpfdh","publishDate":"2026-07-31T06:11:52"}],"_links":{"next":"https://capi.belga.press/belgapress/api/public/pressreleases?offset=7&count=6&search=&start=&end=&newsroomId=70&language=EN","prev":"https://capi.belga.press/belgapress/api/public/pressreleases?offset=-5&count=6&search=&start=&end=&newsroomId=70&language=EN","self":"https://capi.belga.press/belgapress/api/public/pressreleases?offset=1&count=6&search=&start=&end=&newsroomId=70&language=EN"},"_meta":{"total":14650}} \ No newline at end of file diff --git a/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php b/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php new file mode 100644 index 00000000..aeacb5a0 --- /dev/null +++ b/tests/Unit/Services/Parsers/BelgaHomepageParserTest.php @@ -0,0 +1,104 @@ +fixture()); + + $this->assertCount(6, $urls); + $this->assertSame('https://www.belganewsagency.eu/press-releases/35285/', $urls[0]); + $this->assertContains('https://www.belganewsagency.eu/press-releases/35269/', $urls); + } + + public function test_deduplicates_urls(): void + { + $json = json_encode(['data' => [ + ['id' => 100], + ['id' => 100], + ['id' => '100'], + ['id' => 101], + ]]); + + $urls = BelgaHomepageParser::extractArticleUrls($json); + + $this->assertSame([ + 'https://www.belganewsagency.eu/press-releases/100/', + 'https://www.belganewsagency.eu/press-releases/101/', + ], $urls); + } + + public function test_returns_empty_array_for_malformed_json(): void + { + $this->assertSame([], BelgaHomepageParser::extractArticleUrls('404 not found')); + $this->assertSame([], BelgaHomepageParser::extractArticleUrls('{"data": [')); + $this->assertSame([], BelgaHomepageParser::extractArticleUrls('')); + } + + public function test_returns_empty_array_when_data_key_missing_or_empty(): void + { + $this->assertSame([], BelgaHomepageParser::extractArticleUrls('{}')); + $this->assertSame([], BelgaHomepageParser::extractArticleUrls('{"data": []}')); + $this->assertSame([], BelgaHomepageParser::extractArticleUrls('{"data": "not-an-array"}')); + } + + public function test_skips_entries_without_usable_id(): void + { + $json = json_encode(['data' => [ + ['id' => 200], + ['id' => null], + ['publishDate' => '2026-08-01T06:24:19'], + ['id' => 'not-numeric'], + ['id' => 201], + ]]); + + $urls = BelgaHomepageParser::extractArticleUrls($json); + + $this->assertSame([ + 'https://www.belganewsagency.eu/press-releases/200/', + 'https://www.belganewsagency.eu/press-releases/201/', + ], $urls); + } + + public function test_adapter_delegates_and_reports_source(): void + { + $adapter = new BelgaHomepageParserAdapter('en'); + + $this->assertSame( + BelgaHomepageParser::extractArticleUrls($this->fixture()), + $adapter->extractArticleUrls($this->fixture()) + ); + + $this->assertTrue($adapter->canParse('https://www.belganewsagency.eu/press-releases/35285/')); + $this->assertTrue($adapter->canParse('https://capi.belga.press/belgapress/api/public/pressreleases')); + $this->assertFalse($adapter->canParse('https://www.vrt.be/vrtnws/en/')); + $this->assertSame('Belga News Agency', $adapter->getSourceName()); + } + + public function test_adapter_uppercases_language_in_api_url(): void + { + $this->assertStringContainsString('language=EN', (new BelgaHomepageParserAdapter('en'))->getHomepageUrl()); + $this->assertStringContainsString('language=NL', (new BelgaHomepageParserAdapter('nl'))->getHomepageUrl()); + } + + public function test_adapter_api_url_pins_load_bearing_query_params(): void + { + $url = (new BelgaHomepageParserAdapter('en'))->getHomepageUrl(); + + $this->assertStringStartsWith('https://capi.belga.press/belgapress/api/public/pressreleases?', $url); + $this->assertStringContainsString('newsroomId=70', $url); + $this->assertStringContainsString('offset=1', $url); + $this->assertStringContainsString('count=6', $url); + } +}