Skip to content

Commit 920abc2

Browse files
lcharetteCopilot
andcommitted
Complete test coverage
Co-authored-by: Copilot <copilot@github.com>
1 parent 935e7bb commit 920abc2

18 files changed

Lines changed: 660 additions & 242 deletions

app/assets/SearchComponent.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,17 @@ describe('SearchComponent', () => {
142142
expect(wrapper.text()).toContain('Getting Started')
143143
expect(wrapper.text()).toContain('Installation')
144144
})
145+
146+
it('shows error message when the API call fails', async () => {
147+
vi.mocked(axios.get).mockRejectedValue({ response: { data: 'Server error' } })
148+
149+
const wrapper = mount(SearchComponent)
150+
const searchInput = wrapper.find('input[autofocus]')
151+
await searchInput.setValue('test')
152+
153+
await new Promise((resolve) => setTimeout(resolve, 0))
154+
await wrapper.vm.$nextTick()
155+
156+
expect(wrapper.find('.uk-alert-danger').exists()).toBe(true)
157+
})
145158
})

app/src/Controller/DocumentationController.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,16 @@ public function imageVersioned(string $version, string $path, Response $response
8787
$imageResource = $this->pagesDirectory->getVersionedImage($version, $path);
8888

8989
// Get the image content
90-
$imageContent = file_get_contents($imageResource->getAbsolutePath());
91-
92-
if ($imageContent === false) {
90+
$absolutePath = $imageResource->getAbsolutePath();
91+
if (!is_readable($absolutePath)) {
9392
$response->getBody()->write('Image not found');
9493

9594
return $response->withStatus(404);
9695
}
9796

97+
/** @var string $imageContent */
98+
$imageContent = file_get_contents($absolutePath);
99+
98100
// Determine MIME type based on file extension
99101
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
100102
$mimeType = match ($extension) {

app/src/MyRoutes.php

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace UserFrosting\Learn;
1212

13+
use Psr\Http\Message\ResponseInterface as Response;
1314
use Slim\App;
1415
use UserFrosting\Learn\Controller\DocumentationController;
1516
use UserFrosting\Learn\Controller\SearchController;
@@ -32,27 +33,27 @@ public function register(App $app): void
3233
->add(TwigGlobals::class)
3334
->setName('documentation.image');
3435

35-
// Route for versioned and non-versioned documentation pages (with and without trailing slash)
36-
$app->get('/{version:\d+\.\d+}[/{path:.*}]', [DocumentationController::class, 'pageVersioned'])
37-
->add(TwigGlobals::class)
38-
->setName('documentation.versioned');
39-
$app->get('[/{path:.*}]', [DocumentationController::class, 'page'])
40-
->add(TwigGlobals::class)
41-
->setName('documentation');
42-
43-
// Redirect path that ends with a slash to the same path without the slash
44-
$app->get('/{version:\d+\.\d+}/{path:.*}/', function ($request, $response, array $args) use ($app) {
45-
$version = $args['version'];
46-
$path = rtrim($args['path'] ?? '', '/');
36+
// Redirect paths that end with a slash to the same path without the slash.
37+
// These must be registered before the page routes so they take priority.
38+
$app->get('/{version:\d+\.\d+}/{path:.*}/', function (Response $response, string $version, string $path) {
39+
$path = rtrim($path, '/');
4740
$target = '/' . $version . ($path !== '' ? '/' . $path : '');
4841

49-
return $app->redirect($response, $target, 301);
42+
return $response->withHeader('Location', $target)->withStatus(301);
5043
});
51-
$app->get('/{path:.*}/', function ($request, $response, array $args) use ($app) {
52-
$path = rtrim($args['path'] ?? '', '/');
44+
$app->get('/{path:.*}/', function (Response $response, string $path) {
45+
$path = rtrim($path, '/');
5346
$target = $path === '' ? '/' : '/' . $path;
5447

55-
return $app->redirect($response, $target, 301);
48+
return $response->withHeader('Location', $target)->withStatus(301);
5649
});
50+
51+
// Route for versioned and non-versioned documentation pages
52+
$app->get('/{version:\d+\.\d+}[/{path:.*}]', [DocumentationController::class, 'pageVersioned'])
53+
->add(TwigGlobals::class)
54+
->setName('documentation.versioned');
55+
$app->get('[/{path:.*}]', [DocumentationController::class, 'page'])
56+
->add(TwigGlobals::class)
57+
->setName('documentation');
5758
}
5859
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* UserFrosting Learn (http://www.userfrosting.com)
7+
*
8+
* @link https://github.com/userfrosting/Learn
9+
* @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette
10+
* @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License)
11+
*/
12+
13+
namespace UserFrosting\Tests\Learn\Bakery;
14+
15+
use PHPUnit\Framework\TestCase;
16+
use UserFrosting\Learn\Bakery\BakeCommandListener;
17+
use UserFrosting\Learn\Bakery\DebugCommandListener;
18+
use UserFrosting\Learn\Bakery\DebugVerboseCommandListener;
19+
use UserFrosting\Learn\Bakery\SetupCommandListener;
20+
use UserFrosting\Sprinkle\Core\Bakery\Event\BakeCommandEvent;
21+
use UserFrosting\Sprinkle\Core\Bakery\Event\DebugCommandEvent;
22+
use UserFrosting\Sprinkle\Core\Bakery\Event\DebugVerboseCommandEvent;
23+
use UserFrosting\Sprinkle\Core\Bakery\Event\SetupCommandEvent;
24+
25+
/**
26+
* Tests for Bakery event listeners.
27+
*/
28+
class BakeryListenersTest extends TestCase
29+
{
30+
public function testBakeCommandListener(): void
31+
{
32+
$event = new BakeCommandEvent();
33+
$listener = new BakeCommandListener();
34+
$listener($event);
35+
36+
$this->assertSame(
37+
['debug', 'assets:build', 'clear-cache', 'search:index'],
38+
$event->getCommands()
39+
);
40+
}
41+
42+
public function testDebugCommandListener(): void
43+
{
44+
$event = new DebugCommandEvent();
45+
$listener = new DebugCommandListener();
46+
$listener($event);
47+
48+
$this->assertSame(
49+
['debug:version', 'sprinkle:list'],
50+
$event->getCommands()
51+
);
52+
}
53+
54+
public function testDebugVerboseCommandListener(): void
55+
{
56+
$event = new DebugVerboseCommandEvent();
57+
$listener = new DebugVerboseCommandListener();
58+
$listener($event);
59+
60+
$this->assertSame(
61+
['debug:locator', 'debug:events', 'debug:twig'],
62+
$event->getCommands()
63+
);
64+
}
65+
66+
public function testSetupCommandListener(): void
67+
{
68+
$event = new SetupCommandEvent();
69+
$listener = new SetupCommandListener();
70+
$listener($event);
71+
72+
$this->assertSame(
73+
['setup:env'],
74+
$event->getCommands()
75+
);
76+
}
77+
}

app/tests/Controller/DocumentationControllerTest.php

Lines changed: 188 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,210 @@
1010

1111
namespace UserFrosting\Tests\Learn\Controller;
1212

13+
use UserFrosting\Config\Config;
1314
use UserFrosting\Learn\Recipe;
1415
use UserFrosting\Testing\TestCase;
16+
use UserFrosting\UniformResourceLocator\ResourceLocatorInterface;
17+
use UserFrosting\UniformResourceLocator\ResourceStream;
1518

1619
/**
17-
* Tests for AppController Class.
18-
*
19-
* N.B.: This file is sage to edit or delete.
20+
* Tests for DocumentationController class.
2021
*/
2122
class DocumentationControllerTest extends TestCase
2223
{
2324
protected string $mainSprinkle = Recipe::class;
2425

26+
protected function setUp(): void
27+
{
28+
parent::setUp();
29+
30+
/** @var Config $config */
31+
$config = $this->ci->get(Config::class);
32+
$config->set('learn.versions.latest', '6.0');
33+
$config->set('learn.versions.available', ['6.0' => '6.0 Beta']);
34+
35+
/** @var ResourceLocatorInterface $locator */
36+
$locator = $this->ci->get(ResourceLocatorInterface::class);
37+
$locator->removeStream('pages');
38+
$locator->addStream(new ResourceStream('pages', shared: true, readonly: true, path: __DIR__ . '/../pages'));
39+
}
40+
2541
/**
26-
* Test index (`/`) page.
42+
* Test a documentation page renders successfully (non-versioned route).
2743
*/
2844
public function testPageIndex(): void
2945
{
30-
// Create request with method and url and fetch response
31-
// $request = $this->createRequest('GET', '/'); TEMP FIX BELOW
32-
$request = $this->createRequest('GET', '/quick-start');
46+
$request = $this->createRequest('GET', '/first');
3347
$response = $this->handleRequest($request);
3448

35-
// Asserts
3649
$this->assertResponseStatus(200, $response);
3750
$this->assertNotSame('', (string) $response->getBody());
3851
}
52+
53+
/**
54+
* Test a documentation page renders successfully (versioned route).
55+
*/
56+
public function testPageVersioned(): void
57+
{
58+
$request = $this->createRequest('GET', '/6.0/first');
59+
$response = $this->handleRequest($request);
60+
61+
$this->assertResponseStatus(200, $response);
62+
$this->assertNotSame('', (string) $response->getBody());
63+
}
64+
65+
/**
66+
* Test serving a JPEG image via the non-versioned route (delegates to imageVersioned).
67+
*/
68+
public function testImage(): void
69+
{
70+
$request = $this->createRequest('GET', '/images/test.jpg');
71+
$response = $this->handleRequest($request);
72+
73+
$this->assertResponseStatus(200, $response);
74+
$this->assertSame('image/jpeg', $response->getHeaderLine('Content-Type'));
75+
}
76+
77+
/**
78+
* Test serving a JPEG image (jpg extension).
79+
*/
80+
public function testImageVersionedJpeg(): void
81+
{
82+
$request = $this->createRequest('GET', '/6.0/images/test.jpg');
83+
$response = $this->handleRequest($request);
84+
85+
$this->assertResponseStatus(200, $response);
86+
$this->assertSame('image/jpeg', $response->getHeaderLine('Content-Type'));
87+
}
88+
89+
/**
90+
* Test serving a PNG image.
91+
*/
92+
public function testImageVersionedPng(): void
93+
{
94+
$request = $this->createRequest('GET', '/6.0/images/test.png');
95+
$response = $this->handleRequest($request);
96+
97+
$this->assertResponseStatus(200, $response);
98+
$this->assertSame('image/png', $response->getHeaderLine('Content-Type'));
99+
}
100+
101+
/**
102+
* Test serving a GIF image.
103+
*/
104+
public function testImageVersionedGif(): void
105+
{
106+
$request = $this->createRequest('GET', '/6.0/images/test.gif');
107+
$response = $this->handleRequest($request);
108+
109+
$this->assertResponseStatus(200, $response);
110+
$this->assertSame('image/gif', $response->getHeaderLine('Content-Type'));
111+
}
112+
113+
/**
114+
* Test serving an SVG image.
115+
*/
116+
public function testImageVersionedSvg(): void
117+
{
118+
$request = $this->createRequest('GET', '/6.0/images/test.svg');
119+
$response = $this->handleRequest($request);
120+
121+
$this->assertResponseStatus(200, $response);
122+
$this->assertSame('image/svg+xml', $response->getHeaderLine('Content-Type'));
123+
}
124+
125+
/**
126+
* Test serving a WebP image.
127+
*/
128+
public function testImageVersionedWebp(): void
129+
{
130+
$request = $this->createRequest('GET', '/6.0/images/test.webp');
131+
$response = $this->handleRequest($request);
132+
133+
$this->assertResponseStatus(200, $response);
134+
$this->assertSame('image/webp', $response->getHeaderLine('Content-Type'));
135+
}
136+
137+
/**
138+
* Test serving a BMP image.
139+
*/
140+
public function testImageVersionedBmp(): void
141+
{
142+
$request = $this->createRequest('GET', '/6.0/images/test.bmp');
143+
$response = $this->handleRequest($request);
144+
145+
$this->assertResponseStatus(200, $response);
146+
$this->assertSame('image/bmp', $response->getHeaderLine('Content-Type'));
147+
}
148+
149+
/**
150+
* Test serving an ICO image.
151+
*/
152+
public function testImageVersionedIco(): void
153+
{
154+
$request = $this->createRequest('GET', '/6.0/images/test.ico');
155+
$response = $this->handleRequest($request);
156+
157+
$this->assertResponseStatus(200, $response);
158+
$this->assertSame('image/x-icon', $response->getHeaderLine('Content-Type'));
159+
}
160+
161+
/**
162+
* Test serving a file with an unknown extension falls back to octet-stream.
163+
*/
164+
public function testImageVersionedDefaultMimeType(): void
165+
{
166+
$request = $this->createRequest('GET', '/6.0/images/test.bin');
167+
$response = $this->handleRequest($request);
168+
169+
$this->assertResponseStatus(200, $response);
170+
$this->assertSame('application/octet-stream', $response->getHeaderLine('Content-Type'));
171+
}
172+
173+
/**
174+
* Test that an unreadable image file returns a 404 response.
175+
*/
176+
public function testImageVersionedNotReadable(): void
177+
{
178+
if (function_exists('posix_getuid') && posix_getuid() === 0) {
179+
$this->markTestSkipped('Cannot test file permissions as root user.');
180+
}
181+
182+
$imagePath = __DIR__ . '/../pages/6.0/images/unreadable.jpg';
183+
file_put_contents($imagePath, 'data');
184+
chmod($imagePath, 0000);
185+
186+
try {
187+
$request = $this->createRequest('GET', '/6.0/images/unreadable.jpg');
188+
$response = $this->handleRequest($request);
189+
$this->assertResponseStatus(404, $response);
190+
} finally {
191+
chmod($imagePath, 0644);
192+
@unlink($imagePath);
193+
}
194+
}
195+
196+
/**
197+
* Test that a versioned URL with a trailing slash is redirected (301).
198+
*/
199+
public function testTrailingSlashRedirectVersioned(): void
200+
{
201+
$request = $this->createRequest('GET', '/6.0/first/');
202+
$response = $this->handleRequest($request);
203+
204+
$this->assertResponseStatus(301, $response);
205+
$this->assertSame('/6.0/first', $response->getHeaderLine('Location'));
206+
}
207+
208+
/**
209+
* Test that a non-versioned URL with a trailing slash is redirected (301).
210+
*/
211+
public function testTrailingSlashRedirect(): void
212+
{
213+
$request = $this->createRequest('GET', '/first/');
214+
$response = $this->handleRequest($request);
215+
216+
$this->assertResponseStatus(301, $response);
217+
$this->assertSame('/first', $response->getHeaderLine('Location'));
218+
}
39219
}

app/tests/Documentation/DocumentationRepositoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function setUp(): void
4646
$locator->addStream(new ResourceStream('pages', shared: true, readonly: true, path: __DIR__ . '/../pages'));
4747

4848
// Make sure setup is ok
49-
$this->assertCount(10, $locator->listResources('pages://'));
49+
$this->assertCount(17, $locator->listResources('pages://'));
5050
}
5151

5252
public function testGetTree(): void

0 commit comments

Comments
 (0)