| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Http;
- use App\Http\View;
- use App\Tests\TestCase;
- use ReflectionClass;
- use ReflectionProperty;
- /**
- * Smoke tests for the Twig-backed View. Phase 19 swapped the bare-PHP renderer
- * for Twig 3 — these confirm that the controllers' (name, $data) call shape
- * still produces HTML containing the markers each page is expected to carry.
- */
- final class TwigViewTest extends TestCase
- {
- private View $view;
- protected function setUp(): void
- {
- parent::setUp();
- $this->view = new View(__DIR__ . '/../../views');
- }
- private function makeUser(bool $isAdmin = true): object
- {
- $u = new \stdClass();
- $u->id = 1;
- $u->email = 'alice@example.com';
- $u->displayName = 'Alice';
- $u->isAdmin = $isAdmin;
- return $u;
- }
- public function testHomeRendersForSignedInAdmin(): void
- {
- $html = $this->view->render('home', [
- 'title' => 'Sprint Planner',
- 'csrfToken' => 'tok',
- 'currentUser' => $this->makeUser(true),
- 'schemaVersion' => 3,
- 'dbPath' => '/tmp/x',
- 'appEnv' => 'production',
- 'oidcConfigured' => false,
- 'localAdminEnabled' => true,
- 'authError' => false,
- 'sprintRows' => [],
- ]);
- self::assertStringContainsString('<title>Sprint Planner</title>', $html);
- self::assertStringContainsString('Sprints', $html);
- self::assertStringContainsString('Alice', $html); // displayName
- self::assertStringContainsString('admin', $html); // admin badge
- self::assertStringContainsString('/auth/logout', $html);
- // Phase 19: vendored Alpine + htmx + sortable scripts must be linked.
- self::assertStringContainsString('/assets/js/vendor/alpine-csp.min.js', $html);
- self::assertStringContainsString('/assets/js/vendor/htmx.min.js', $html);
- }
- public function testHomeRendersDeletedSprintFlashWhenSet(): void
- {
- // R01-N26: the green "Sprint X was deleted" chip is now driven by
- // the `deletedSprintName` view variable (sourced from a one-shot
- // session flash, not from `?deleted=`). Pin that the chip appears
- // for any non-empty string and that the name is HTML-escaped.
- $html = $this->view->render('home', [
- 'title' => 'Sprint Planner',
- 'csrfToken' => 'tok',
- 'currentUser' => $this->makeUser(true),
- 'schemaVersion' => 3,
- 'dbPath' => '/tmp/x',
- 'appEnv' => 'production',
- 'oidcConfigured' => false,
- 'localAdminEnabled' => true,
- 'authError' => false,
- 'deletedSprintName' => 'Sprint <Alpha>',
- 'sprintRows' => [],
- ]);
- self::assertStringContainsString('was deleted', $html);
- self::assertStringContainsString('Sprint <Alpha>', $html);
- self::assertStringNotContainsString(
- '<b>Sprint <Alpha></b>',
- $html,
- 'sprint name must be Twig-autoescaped — never rendered raw',
- );
- }
- public function testHomeOmitsDeletedSprintFlashWhenEmpty(): void
- {
- $html = $this->view->render('home', [
- 'title' => 'Sprint Planner',
- 'csrfToken' => 'tok',
- 'currentUser' => $this->makeUser(true),
- 'schemaVersion' => 3,
- 'dbPath' => '/tmp/x',
- 'appEnv' => 'production',
- 'oidcConfigured' => false,
- 'localAdminEnabled' => true,
- 'authError' => false,
- 'deletedSprintName' => '',
- 'sprintRows' => [],
- ]);
- self::assertStringNotContainsString('was deleted', $html);
- }
- public function testHomeForAnonymousUserHidesRuntimePanel(): void
- {
- $html = $this->view->render('home', [
- 'title' => 'Sprint Planner',
- 'csrfToken' => 'tok',
- 'currentUser' => null,
- 'schemaVersion' => 3,
- 'dbPath' => '/var/data/app.sqlite',
- 'appEnv' => 'production',
- 'oidcConfigured' => true,
- 'localAdminEnabled' => true,
- 'authError' => false,
- 'sprintRows' => [],
- ]);
- self::assertStringContainsString('Sign in with Microsoft', $html);
- // R01-N02: the Runtime <details> panel must not leak PHP_VERSION,
- // dbPath, schema version, OIDC/local-admin flags to anonymous visitors.
- self::assertStringNotContainsString('Runtime', $html);
- self::assertStringNotContainsString('Schema version', $html);
- self::assertStringNotContainsString('/var/data/app.sqlite', $html);
- self::assertStringNotContainsString(PHP_VERSION, $html);
- // R01-N31 falls out of the same gate: no /healthz hint either.
- self::assertStringNotContainsString('/healthz', $html);
- }
- public function testAuditRendersWithEmptyResults(): void
- {
- $html = $this->view->render('audit/index', [
- 'title' => 'Audit',
- 'csrfToken' => 'tok',
- 'currentUser' => $this->makeUser(true),
- 'filters' => [
- 'user_email' => '',
- 'action' => '',
- 'entity_type' => '',
- 'entity_id' => '',
- 'from_date' => '',
- 'to_date' => '',
- ],
- 'page' => 1,
- 'pages' => 1,
- 'pageSize' => 50,
- 'total' => 0,
- 'rows' => [],
- 'actions' => ['CREATE', 'UPDATE', 'DELETE'],
- 'entityTypes' => ['worker', 'sprint'],
- 'users' => [],
- ]);
- self::assertStringContainsString('Audit log', $html);
- self::assertStringContainsString('No audit rows match', $html);
- // hx-boost wired on the filter form (Phase 19).
- self::assertStringContainsString('hx-boost="true"', $html);
- }
- public function testSprintsShowRendersTaskGridAndStatusFilter(): void
- {
- $sprint = (new ReflectionClass(\App\Domain\Sprint::class))->newInstanceWithoutConstructor();
- foreach ([
- 'id' => 1, 'name' => 'S1',
- 'startDate' => '2026-01-01', 'endDate' => '2026-01-15',
- 'reserveFraction' => 0.2, 'isArchived' => false, 'createdAt' => '2026-01-01',
- ] as $k => $v) {
- (new ReflectionProperty($sprint, $k))->setValue($sprint, $v);
- }
- $weekClass = new ReflectionClass(\App\Domain\SprintWeek::class);
- $w = $weekClass->newInstanceWithoutConstructor();
- foreach ([
- 'id' => 10, 'sprintId' => 1, 'isoWeek' => 1,
- 'startDate' => '2026-01-05', 'sortOrder' => 1,
- 'maxWorkingDays' => 5, 'activeDaysMask' => 31,
- ] as $k => $v) {
- (new ReflectionProperty($w, $k))->setValue($w, $v);
- }
- $swClass = new ReflectionClass(\App\Domain\SprintWorker::class);
- $sw = $swClass->newInstanceWithoutConstructor();
- foreach ([
- 'id' => 100, 'sprintId' => 1, 'workerId' => 50, 'workerName' => 'Bob',
- 'sortOrder' => 1, 'rtb' => 0.1,
- ] as $k => $v) {
- (new ReflectionProperty($sw, $k))->setValue($sw, $v);
- }
- $html = $this->view->render('sprints/show', [
- 'title' => 'S1',
- 'csrfToken' => 'tok',
- 'currentUser' => $this->makeUser(true),
- 'sprint' => $sprint,
- 'weeks' => [$w],
- 'sprintWorkers' => [$sw],
- 'grid' => [100 => [10 => 2.5]],
- 'capacity' => [100 => [
- 'ressourcen' => 2.5,
- 'after_reserves' => 2.0,
- 'committed_prio1' => 0.0,
- 'available' => 2.0,
- ]],
- 'tasks' => [],
- 'taskGrid' => [],
- 'statusGrid' => [],
- 'ownerChoices' => [],
- 'taskStatusEnabled' => true,
- ]);
- self::assertStringContainsString('data-sprint-root', $html);
- self::assertStringContainsString('data-sprint-id="1"', $html);
- self::assertStringContainsString('data-task-status-enabled="1"', $html);
- self::assertStringContainsString('Status', $html); // status filter visible
- self::assertStringContainsString('No tasks yet', $html);
- // Phase 19: page-specific JS still loaded.
- self::assertStringContainsString('/assets/js/sprint-planner.js', $html);
- }
- }
|