method; } public function getUri(): UriInterface { $path = $this->path; return new class ($path) implements UriInterface { public function __construct(private readonly string $path) {} public function getScheme(): string { return 'https'; } public function getAuthority(): string { return ''; } public function getUserInfo(): string { return ''; } public function getHost(): string { return 'localhost'; } public function getPort(): ?int { return null; } public function getPath(): string { return $this->path; } public function getQuery(): string { return ''; } public function getFragment(): string { return ''; } public function withScheme(string $scheme): static { return clone $this; } public function withUserInfo(string $user, ?string $password = null): static { return clone $this; } public function withHost(string $host): static { return clone $this; } public function withPort(?int $port): static { return clone $this; } public function withPath(string $path): static { return clone $this; } public function withQuery(string $query): static { return clone $this; } public function withFragment(string $fragment): static { return clone $this; } public function __toString(): string { return $this->path; } }; } public function getBody(): StreamInterface { $body = $this->body; return new class ($body) implements StreamInterface { public function __construct(private readonly string $content) {} public function __toString(): string { return $this->content; } public function close(): void {} public function detach() { return null; } public function getSize(): ?int { return strlen($this->content); } public function tell(): int { return 0; } public function eof(): bool { return true; } public function isSeekable(): bool { return false; } public function seek(int $offset, int $whence = SEEK_SET): void {} public function rewind(): void {} public function isWritable(): bool { return false; } public function write(string $string): int { return 0; } public function isReadable(): bool { return true; } public function read(int $length): string { return $this->content; } public function getContents(): string { return $this->content; } public function getMetadata(?string $key = null): mixed { return null; } }; } public function getQueryParams(): array { return $this->queryParams; } public function getServerParams(): array { return $this->serverParams; } public function getHeaderLine(string $name): string { foreach ($this->headers as $key => $value) { if (strcasecmp($key, $name) === 0) { return $value; } } return ''; } public function getHeader(string $name): array { foreach ($this->headers as $key => $value) { if (strcasecmp($key, $name) === 0) { return [$value]; } } return []; } public function hasHeader(string $name): bool { foreach ($this->headers as $key => $value) { if (strcasecmp($key, $name) === 0) { return true; } } return false; } public function getHeaders(): array { return $this->headers; } public function getAttribute(string $name, mixed $default = null): mixed { return $this->attributes[$name] ?? $default; } public function withAttribute(string $name, mixed $value): static { $clone = clone $this; $clone->attributes[$name] = $value; return $clone; } public function getRequestTarget(): string { return $this->path; } public function withRequestTarget(string $requestTarget): static { return clone $this; } public function withMethod(string $method): static { return clone $this; } public function withUri(UriInterface $uri, bool $preserveHost = false): static { return clone $this; } public function getProtocolVersion(): string { return '1.1'; } public function withProtocolVersion(string $version): static { return clone $this; } public function withHeader(string $name, $value): static { return clone $this; } public function withAddedHeader(string $name, $value): static { return clone $this; } public function withoutHeader(string $name): static { return clone $this; } public function withBody(StreamInterface $body): static { return clone $this; } public function getCookieParams(): array { return []; } public function withCookieParams(array $cookies): static { return clone $this; } public function withQueryParams(array $query): static { return clone $this; } public function getUploadedFiles(): array { return []; } public function withUploadedFiles(array $uploadedFiles): static { return clone $this; } public function getParsedBody(): mixed { return null; } public function withParsedBody($data): static { return clone $this; } public function getAttributes(): array { return $this->attributes; } public function withoutAttribute(string $name): static { $clone = clone $this; unset($clone->attributes[$name]); return $clone; } }; } /** * Create a Config instance from a nested data array. * * Returns a real Grav\Common\Config\Config (or our stub equivalent). */ public static function createMockConfig(array $data = []): Config { return new Config($data); } /** * Create a mock user that duck-types Grav\Common\User\Interfaces\UserInterface. * * The returned object has a public $username property and supports * get(), set(), save(), and exists(). */ public static function createMockUser( string $username = 'testuser', array $data = [], bool $exists = true, ): UserInterface { return new class ($username, $data, $exists) implements UserInterface { public readonly string $username; public function __construct( string $username, private array $data, private readonly bool $existsFlag, ) { $this->username = $username; } public function get(string $key, mixed $default = null): mixed { return $this->data[$key] ?? $default; } public function set(string $key, mixed $value): void { $this->data[$key] = $value; } public function save(): void { // no-op in tests } public function exists(): bool { return $this->existsFlag; } }; } /** * Create a Grav container instance with given services. * * Returns the Grav singleton (reset between calls). */ public static function createMockGrav(array $services = []): Grav { Grav::resetInstance(); $grav = Grav::instance(); foreach ($services as $key => $value) { $grav[$key] = $value; } return $grav; } /** * Create a mock accounts collection that is iterable and supports load(). */ public static function createMockAccounts(array $users = []): object { return new class ($users) implements \IteratorAggregate { /** @param array $users keyed by username */ public function __construct(private readonly array $users) {} public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->users); } public function load(string $username): object { return $this->users[$username] ?? self::nonExistentUser($username); } private static function nonExistentUser(string $username): object { return new class ($username) { public readonly string $username; public function __construct(string $username) { $this->username = $username; } public function exists(): bool { return false; } public function get(string $key, mixed $default = null): mixed { return $default; } }; } }; } }