diff --git a/assets/styles/backoffice.scss b/assets/styles/backoffice.scss index cb69351..70f45b4 100644 --- a/assets/styles/backoffice.scss +++ b/assets/styles/backoffice.scss @@ -3,3 +3,9 @@ .col-result-md { width: 20%; } .modal-content > turbo-frame { display: contents; } + +.release-notes { + overflow-wrap: break-word; + + > :last-child { margin-bottom: 0; } +} diff --git a/composer.json b/composer.json index 322857e..8572c9f 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "doctrine/doctrine-bundle": "^3.2.2", "doctrine/doctrine-migrations-bundle": "^4.0", "doctrine/orm": "^3.6.2", + "league/commonmark": "^2.7", "martin-georgiev/postgresql-for-doctrine": "^4.4", "phpdocumentor/reflection-docblock": "^6.0.3", "phpoffice/phpspreadsheet": "^5.5", @@ -27,6 +28,7 @@ "symfony/flex": "^2.11.0", "symfony/form": "8.1.*", "symfony/framework-bundle": "8.1.*", + "symfony/http-client": "8.1.*", "symfony/mailer": "8.1.*", "symfony/object-mapper": "8.1.*", "symfony/property-access": "8.1.*", @@ -48,6 +50,7 @@ "thecodingmachine/safe": "^3.4.0", "twig/extra-bundle": "^3.24.0", "twig/intl-extra": "^3.24.0", + "twig/markdown-extra": "^3.24.0", "twig/twig": "^3.27.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 5cbc0a7..53c3c3f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ccae654dd9c952e8920d9cb9c0f35ff5", + "content-hash": "519e76dd9df3bca43c3e32e347c600e7", "packages": [ { "name": "composer/pcre", @@ -159,6 +159,81 @@ ], "time": "2025-08-20T19:15:30+00:00" }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, { "name": "doctrine/collections", "version": "2.6.0", @@ -1652,6 +1727,195 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "league/commonmark", + "version": "2.8.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7", + "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-07-12T15:29:16+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.2.2", @@ -1966,6 +2230,164 @@ ], "time": "2026-07-01T18:17:39+00:00" }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -8711,6 +9133,78 @@ ], "time": "2026-05-19T20:44:48+00:00" }, + { + "name": "twig/markdown-extra", + "version": "v3.28.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/markdown-extra.git", + "reference": "5f7b27e41a382fc988fffa6e588d8f9d55b9d896" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/5f7b27e41a382fc988fffa6e588d8f9d55b9d896", + "reference": "5f7b27e41a382fc988fffa6e588d8f9d55b9d896", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "twig/twig": "^3.13|^4.0" + }, + "require-dev": { + "erusev/parsedown": "dev-master as 1.x-dev", + "league/commonmark": "^2.7", + "league/html-to-markdown": "^4.8|^5.0", + "michelf/php-markdown": "^1.8|^2.0", + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Twig\\Extra\\Markdown\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Markdown", + "homepage": "https://twig.symfony.com", + "keywords": [ + "html", + "markdown", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/markdown-extra/tree/v3.28.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-06-05T19:47:22+00:00" + }, { "name": "twig/twig", "version": "v3.28.0", diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b58f752..378d16d 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -30,6 +30,7 @@ security: access_control: - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/backoffice/releases$, roles: PUBLIC_ACCESS } - { path: ^/backoffice, roles: IS_AUTHENTICATED } when@test: diff --git a/config/packages/twig_extra.yaml b/config/packages/twig_extra.yaml new file mode 100644 index 0000000..af379df --- /dev/null +++ b/config/packages/twig_extra.yaml @@ -0,0 +1,8 @@ +twig_extra: + commonmark: + allow_unsafe_links: false + html_input: escape + +services: + League\CommonMark\Extension\Autolink\AutolinkExtension: + tags: ['twig.markdown.league_extension'] diff --git a/config/reference.php b/config/reference.php index 2847703..5a712c0 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1271,7 +1271,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * enabled?: bool|Param, // Default: false * }, * markdown?: bool|array{ - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * }, * intl?: bool|array{ * enabled?: bool|Param, // Default: true diff --git a/frankenphp/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh index 07746b4..595f33a 100755 --- a/frankenphp/docker-entrypoint.sh +++ b/frankenphp/docker-entrypoint.sh @@ -33,6 +33,10 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then fi fi + # var/ is a Docker volume that survives redeploys, so cache.app (e.g. the GitHub releases cache) + # can carry stale entries from the previous image into the new one unless cleared here. + php bin/console cache:pool:clear cache.app + setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var diff --git a/src/Controller/Backoffice/ReleasesController.php b/src/Controller/Backoffice/ReleasesController.php new file mode 100644 index 0000000..b24fc37 --- /dev/null +++ b/src/Controller/Backoffice/ReleasesController.php @@ -0,0 +1,25 @@ +render('backoffice/releases/_frame.html.twig', [ + 'releases' => $this->gitHubReleasesService->getReleases(), + ]); + } +} diff --git a/src/Service/GitHubReleasesService.php b/src/Service/GitHubReleasesService.php new file mode 100644 index 0000000..a53045a --- /dev/null +++ b/src/Service/GitHubReleasesService.php @@ -0,0 +1,65 @@ + + */ + public function getReleases(): array + { + return $this->cache->get('github_releases', $this->fetchReleases(...)); + } + + /** @return list */ + private function fetchReleases(ItemInterface $item, bool &$save): array + { + try { + $response = $this->httpClient->request('GET', self::RELEASES_URL, [ + 'timeout' => 5, + 'headers' => [ + 'Accept' => 'application/vnd.github+json', + 'User-Agent' => 'TijdVoorDeTest', + ], + ]); + + /** @var list $releases */ + $releases = $response->toArray(); + } catch (ExceptionInterface) { + $save = false; + + return []; + } + + $item->expiresAfter(3600); + + usort($releases, static fn (array $a, array $b): int => ($b['published_at'] ?? '') <=> ($a['published_at'] ?? '')); + + return array_map(static fn (array $release): array => [ + 'tagName' => $release['tag_name'], + 'name' => $release['name'] ?: $release['tag_name'], + 'publishedAt' => $release['published_at'] ? new DateTimeImmutable($release['published_at']) : null, + 'body' => (string) $release['body'], + 'url' => $release['html_url'], + ], $releases); + } +} diff --git a/templates/backoffice/nav.html.twig b/templates/backoffice/nav.html.twig index 18fee18..b6c2e95 100644 --- a/templates/backoffice/nav.html.twig +++ b/templates/backoffice/nav.html.twig @@ -10,9 +10,9 @@ aria-label="Toggle navigation"> - {% if is_granted('IS_AUTHENTICATED') %} - diff --git a/templates/backoffice/releases/_frame.html.twig b/templates/backoffice/releases/_frame.html.twig new file mode 100644 index 0000000..b2890ca --- /dev/null +++ b/templates/backoffice/releases/_frame.html.twig @@ -0,0 +1,40 @@ + + + + diff --git a/tests/Controller/Backoffice/ReleasesControllerTest.php b/tests/Controller/Backoffice/ReleasesControllerTest.php new file mode 100644 index 0000000..030d969 --- /dev/null +++ b/tests/Controller/Backoffice/ReleasesControllerTest.php @@ -0,0 +1,55 @@ +loginAs('user2@example.org'); + $this->mockReleasesService(); + + $this->client->request(Request::METHOD_GET, '/backoffice/releases'); + + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'v0.8.0'); + self::assertSelectorTextContains('body', 'Some release notes'); + self::assertSelectorTextContains('#releasesModalLabel', 'Huidige versie: v0.8.0'); + } + + public function testReleasesFrameIsAccessibleWithoutAuthentication(): void + { + $this->mockReleasesService(); + + $this->client->request(Request::METHOD_GET, '/backoffice/releases'); + + self::assertResponseIsSuccessful(); + } + + private function mockReleasesService(): void + { + $body = json_encode([ + [ + 'tag_name' => 'v0.8.0', + 'name' => 'v0.8.0', + 'published_at' => '2026-07-12T10:00:00Z', + 'body' => 'Some release notes', + 'html_url' => 'https://github.com/MarijnDoeve/TijdVoorDeTest/releases/tag/v0.8.0', + ], + ], \JSON_THROW_ON_ERROR); + $httpClient = new MockHttpClient([new MockResponse((string) $body, ['response_headers' => ['content-type' => 'application/json']])]); + self::getContainer()->set(GitHubReleasesService::class, new GitHubReleasesService($httpClient, new ArrayAdapter())); + } +} diff --git a/tests/Service/GitHubReleasesServiceTest.php b/tests/Service/GitHubReleasesServiceTest.php new file mode 100644 index 0000000..3b6f762 --- /dev/null +++ b/tests/Service/GitHubReleasesServiceTest.php @@ -0,0 +1,95 @@ + 'v0.8.0', + 'name' => 'v0.8.0', + 'published_at' => '2026-07-12T10:00:00Z', + 'body' => "## Added\n- Something new", + 'html_url' => 'https://github.com/MarijnDoeve/TijdVoorDeTest/releases/tag/v0.8.0', + ], + ], \JSON_THROW_ON_ERROR); + + $httpClient = new MockHttpClient([new MockResponse((string) $body, ['response_headers' => ['content-type' => 'application/json']])]); + $subject = new GitHubReleasesService($httpClient, new ArrayAdapter()); + + $releases = $subject->getReleases(); + + $this->assertEquals([ + 'tagName' => 'v0.8.0', + 'name' => 'v0.8.0', + 'publishedAt' => new DateTimeImmutable('2026-07-12T10:00:00Z'), + 'body' => "## Added\n- Something new", + 'url' => 'https://github.com/MarijnDoeve/TijdVoorDeTest/releases/tag/v0.8.0', + ], $releases[0]); + } + + public function testGetReleasesReturnsEmptyArrayOnHttpFailure(): void + { + $httpClient = new MockHttpClient(static fn (): MockResponse => new MockResponse('', ['http_code' => 500])); + $subject = new GitHubReleasesService($httpClient, new ArrayAdapter()); + + $this->assertSame([], $subject->getReleases()); + } + + public function testHttpFailureIsNotCached(): void + { + $requestCount = 0; + $httpClient = new MockHttpClient(static function () use (&$requestCount): MockResponse { + ++$requestCount; + + return new MockResponse('', ['http_code' => 500]); + }); + $subject = new GitHubReleasesService($httpClient, new ArrayAdapter()); + + $subject->getReleases(); + $subject->getReleases(); + + $this->assertSame(2, $requestCount); + } + + public function testGetReleasesSortsNewestFirst(): void + { + $body = json_encode([ + [ + 'tag_name' => 'v0.7.0', + 'name' => 'v0.7.0', + 'published_at' => '2026-07-10T10:00:00Z', + 'body' => 'Older release', + 'html_url' => 'https://github.com/MarijnDoeve/TijdVoorDeTest/releases/tag/v0.7.0', + ], + [ + 'tag_name' => 'v0.8.0', + 'name' => 'v0.8.0', + 'published_at' => '2026-07-12T10:00:00Z', + 'body' => 'Newer release', + 'html_url' => 'https://github.com/MarijnDoeve/TijdVoorDeTest/releases/tag/v0.8.0', + ], + ], \JSON_THROW_ON_ERROR); + + $httpClient = new MockHttpClient([new MockResponse((string) $body, ['response_headers' => ['content-type' => 'application/json']])]); + $subject = new GitHubReleasesService($httpClient, new ArrayAdapter()); + + $releases = $subject->getReleases(); + + $this->assertSame('v0.8.0', $releases[0]['tagName']); + $this->assertSame('v0.7.0', $releases[1]['tagName']); + } +} diff --git a/translations/messages+intl-icu.nl.xliff b/translations/messages+intl-icu.nl.xliff index 76eb9ec..5936b45 100644 --- a/translations/messages+intl-icu.nl.xliff +++ b/translations/messages+intl-icu.nl.xliff @@ -241,6 +241,10 @@ Could not find candidate with name {name} in elimination. Kon geen kandidaat vinden met de naam {name} in de eliminatie + + Could not load releases from GitHub. + Kan releases niet laden van GitHub. + Create a season Maak een seizoen aan @@ -265,6 +269,10 @@ Current password Huidig wachtwoord + + Current version + Huidige versie + Cyan Cyaan @@ -745,6 +753,10 @@ Register Registreren + + Releases + Releases + Remember me Onthoud mij @@ -977,6 +989,10 @@ View Bekijken + + View on GitHub + Bekijk op GitHub + White Wit