Files
intotheeast-com-content/plugins/api/tests/Unit/Controllers/PagesControllerReorganizeTest.php
T

281 lines
9.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Grav\Plugin\Api\Tests\Unit\Controllers;
use Grav\Common\Config\Config;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Plugin\Api\Controllers\PagesController;
use Grav\Plugin\Api\Exceptions\ValidationException;
use Grav\Plugin\Api\Tests\Unit\TestHelper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for the PagesController::reorganize() validation logic.
*
* Tests exercise the validation phase without performing filesystem operations.
*/
#[CoversClass(PagesController::class)]
class PagesControllerReorganizeTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
$this->tempDir = sys_get_temp_dir() . '/grav_api_reorg_test_' . uniqid();
@mkdir($this->tempDir . '/cache/api/thumbnails', 0775, true);
@mkdir($this->tempDir . '/pages', 0775, true);
}
protected function tearDown(): void
{
// Clean up temp dirs
$this->rmrf($this->tempDir);
}
private function rmrf(string $dir): void
{
if (!is_dir($dir)) return;
foreach (scandir($dir) as $item) {
if ($item === '.' || $item === '..') continue;
$path = $dir . '/' . $item;
is_dir($path) ? $this->rmrf($path) : unlink($path);
}
rmdir($dir);
}
private function createController(array $knownPages = [], array $configOverrides = []): PagesController
{
$pagesService = $this->createPagesService($knownPages);
$tempDir = $this->tempDir;
$config = new Config(array_merge([
'plugins' => ['api' => [
'batch' => ['max_items' => 50],
'route' => '/api',
'version_prefix' => 'v1',
]],
], $configOverrides));
$locator = new class ($tempDir) {
public function __construct(private string $base) {}
public function findResource(string $uri, bool $absolute = false): ?string
{
return match (true) {
str_starts_with($uri, 'cache://') => $this->base . '/cache',
str_starts_with($uri, 'page://') => $this->base . '/pages',
default => $this->base,
};
}
};
$grav = TestHelper::createMockGrav([
'pages' => $pagesService,
'config' => $config,
'locator' => $locator,
]);
return new PagesController($grav, $config);
}
private function createPagesService(array $knownPages): object
{
return new class ($knownPages) {
private array $pages;
public function __construct(array $pages) { $this->pages = $pages; }
public function enablePages(): void {}
public function reset(): void {}
public function find(string $route): ?object
{
return $this->pages[$route] ?? null;
}
};
}
private function createMockPage(string $route, string $slug, ?int $order = null): object
{
$pagesDir = $this->tempDir . '/pages';
$path = $pagesDir . '/' . ($order !== null ? str_pad((string)$order, 2, '0', STR_PAD_LEFT) . '.' . $slug : $slug);
return new class ($route, $slug, $order, $path) {
public function __construct(
private string $route,
private string $slug,
private ?int $order,
private string $path,
) {}
public function route($var = null): ?string { return $this->route; }
public function slug($var = null): string { return $this->slug; }
public function order($var = null): ?int { return $this->order; }
public function path($var = null): ?string { return $this->path; }
public function title($var = null): string { return ucfirst($this->slug); }
public function isModule(): bool { return false; }
public function children(): \Traversable { return new \ArrayIterator([]); }
};
}
private function makeRequest(array $body): \Psr\Http\Message\ServerRequestInterface
{
// API authority is scoped to access.api.super (admin-classic's legacy
// access.admin.super is intentionally NOT honored by the API — see
// AbstractApiController::isSuperAdmin()).
$superAdmin = TestHelper::createMockUser('admin', [
'access.api.super' => true,
]);
return TestHelper::createMockRequest(
method: 'POST',
path: '/api/v1/pages/reorganize',
headers: ['Content-Type' => 'application/json'],
body: json_encode($body),
attributes: [
'api_user' => $superAdmin,
'json_body' => $body,
],
);
}
// -------------------------------------------------------
// Validation tests
// -------------------------------------------------------
#[Test]
public function reorganize_requires_operations_field(): void
{
$controller = $this->createController();
$this->expectException(ValidationException::class);
$controller->reorganize($this->makeRequest([]));
}
#[Test]
public function reorganize_rejects_empty_operations_array(): void
{
$controller = $this->createController();
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('non-empty array');
$controller->reorganize($this->makeRequest(['operations' => []]));
}
#[Test]
public function reorganize_rejects_operation_missing_route(): void
{
$controller = $this->createController();
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('route');
$controller->reorganize($this->makeRequest([
'operations' => [
['position' => 1],
],
]));
}
#[Test]
public function reorganize_rejects_duplicate_routes(): void
{
$controller = $this->createController([
'/blog/post-a' => $this->createMockPage('/blog/post-a', 'post-a'),
'/blog' => $this->createMockPage('/blog', 'blog'),
]);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Duplicate');
$controller->reorganize($this->makeRequest([
'operations' => [
['route' => '/blog/post-a', 'position' => 1],
['route' => '/blog/post-a', 'position' => 2],
],
]));
}
#[Test]
public function reorganize_rejects_nonexistent_page(): void
{
$controller = $this->createController();
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('not found');
$controller->reorganize($this->makeRequest([
'operations' => [
['route' => '/does-not-exist', 'position' => 1],
],
]));
}
#[Test]
public function reorganize_rejects_nonexistent_parent(): void
{
$controller = $this->createController([
'/blog/post-a' => $this->createMockPage('/blog/post-a', 'post-a'),
'/blog' => $this->createMockPage('/blog', 'blog'),
]);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Destination parent not found');
$controller->reorganize($this->makeRequest([
'operations' => [
['route' => '/blog/post-a', 'parent' => '/nonexistent', 'position' => 1],
],
]));
}
#[Test]
public function reorganize_rejects_move_into_own_subtree(): void
{
$controller = $this->createController([
'/blog' => $this->createMockPage('/blog', 'blog'),
'/blog/child' => $this->createMockPage('/blog/child', 'child'),
]);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('own subtree');
$controller->reorganize($this->makeRequest([
'operations' => [
['route' => '/blog', 'parent' => '/blog/child'],
],
]));
}
#[Test]
public function reorganize_rejects_position_conflict(): void
{
$controller = $this->createController([
'/blog/post-a' => $this->createMockPage('/blog/post-a', 'post-a'),
'/blog/post-b' => $this->createMockPage('/blog/post-b', 'post-b'),
'/blog' => $this->createMockPage('/blog', 'blog'),
]);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Position conflict');
$controller->reorganize($this->makeRequest([
'operations' => [
['route' => '/blog/post-a', 'position' => 1],
['route' => '/blog/post-b', 'position' => 1],
],
]));
}
#[Test]
public function reorganize_rejects_exceeding_batch_limit(): void
{
$pages = [];
$ops = [];
for ($i = 0; $i < 51; $i++) {
$route = "/page-{$i}";
$pages[$route] = $this->createMockPage($route, "page-{$i}");
$ops[] = ['route' => $route, 'position' => $i + 1];
}
$controller = $this->createController($pages);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('limited to');
$controller->reorganize($this->makeRequest(['operations' => $ops]));
}
}