-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
Unit-test code that uses Cookie without emitting real HTTP headers or
mutating $_COOKIE, by injecting the two test seams the constructor exposes —
the raw $source (read side) and the $writer (write side).
public function __construct(
string $name,
string $salt,
array $options = [],
?array $source = null, // raw cookie source; defaults to $_COOKIE
?callable $writer = null // low-level writer; defaults to setcookie()
);-
$sourceis a raw['cookieName' => 'rawValue']array. Pass it to simulate an incoming cookie instead of reading$_COOKIE. The'rawValue'must be in the on-the-wire format (base64(serialize(entries)) . '.' . hmac), not a plain value. -
$writeris acallable(string $name, string $value, array $options): bool. Pass a recording closure to capture what would be written instead of callingsetcookie(). This also makes the destructor safety-net harmless under test (it calls the recording writer, not real headers).
Capture every emission so assertions can inspect it:
$written = [];
$writer = static function (string $name, string $value, array $options) use (&$written): bool {
$written[] = ['name' => $name, 'value' => $value, 'options' => $options];
return true;
};
$cookie = new InitPHP\Cookies\Cookie('session', 's3cr3t-salt', [], [], $writer);
$cookie->set('user', 'ada');
$cookie->send();
// $written[0]['name'] === 'session'
// $written[0]['value'] is the signed wire stringTo feed an incoming cookie to $source, you must produce the same signed
envelope the manager writes. Each stored entry is
['value' => scalar, 'ttl' => int|null], where ttl is an absolute Unix
timestamp (or null for "no per-key expiry"):
function encodeCookie(array $entries, string $salt): string
{
$payload = base64_encode(serialize($entries));
return $payload . '.' . hash_hmac('sha256', $payload, $salt);
}
$source = ['session' => encodeCookie([
'user' => ['value' => 'ada', 'ttl' => null],
], 's3cr3t-salt')];
$cookie = new InitPHP\Cookies\Cookie('session', 's3cr3t-salt', [], $source);
$cookie->get('user'); // 'ada'This is exactly the pattern the package's own test suite uses; its
CookieTestCase provides the canonical encodeCookie() helper and recording
writer() — including for crafting hostile inputs (expired entries,
tampering, wrong salt).
A small base class mirrors the package's scaffolding and keeps individual tests terse:
use InitPHP\Cookies\Cookie;
use PHPUnit\Framework\TestCase;
abstract class CookieTestCase extends TestCase
{
protected const NAME = 'session';
protected const SALT = 's3cr3t-salt';
/** @var array<int, array{name: string, value: string, options: array}> */
protected array $written = [];
protected function setUp(): void
{
$this->written = [];
}
protected function writer(): callable
{
return function (string $name, string $value, array $options): bool {
$this->written[] = compact('name', 'value', 'options');
return true;
};
}
protected function cookie(array $options = [], array $source = []): Cookie
{
return new Cookie(self::NAME, self::SALT, $options, $source, $this->writer());
}
protected function encodeCookie(array $entries, string $salt = self::SALT): string
{
$payload = base64_encode(serialize($entries));
return $payload . '.' . hash_hmac('sha256', $payload, $salt);
}
protected function lastWrittenValue(string $name = self::NAME): ?string
{
for ($i = count($this->written) - 1; $i >= 0; $i--) {
if ($this->written[$i]['name'] === $name) {
return $this->written[$i]['value'];
}
}
return null;
}
}Capture what an issuer wrote, then feed it back into a reader as $source:
final class MyCookieTest extends CookieTestCase
{
public function testValueSurvivesARoundTrip(): void
{
$issuer = $this->cookie();
$issuer->set('user', 'ada');
$issuer->send();
$reader = $this->cookie([], [self::NAME => $this->lastWrittenValue()]);
self::assertSame('ada', $reader->get('user'));
}
}public function testSendIsANoOpWhenNothingChanged(): void
{
$cookie = $this->cookie();
self::assertTrue($cookie->send());
self::assertCount(0, $this->written); // nothing emitted
}
public function testSendWritesAfterAMutation(): void
{
$cookie = $this->cookie();
$cookie->set('a', '1');
$cookie->send();
self::assertCount(1, $this->written);
self::assertSame(self::NAME, $this->written[0]['name']);
}public function testExpiredEntryIsAbsent(): void
{
$source = [self::NAME => $this->encodeCookie([
'token' => ['value' => 'abc', 'ttl' => time() - 10], // already past
])];
$cookie = $this->cookie([], $source);
self::assertFalse($cookie->has('token'));
}
public function testTamperedCookieIsRejected(): void
{
$valid = $this->encodeCookie(['user' => ['value' => 'ada', 'ttl' => null]]);
$tampered = ($valid[0] === 'A' ? 'B' : 'A') . substr($valid, 1);
$cookie = $this->cookie([], [self::NAME => $tampered]);
self::assertNull($cookie->get('user')); // signature mismatch → discarded
}Type-hint CookieInterface in your own
classes so a test can pass a manager wired to a recording writer:
use InitPHP\Cookies\CookieInterface;
final class LoginService
{
public function __construct(private CookieInterface $cookie) {}
public function login(int $userId): void
{
$this->cookie->set('user_id', $userId);
$this->cookie->send();
}
}
// Test:
$service = new LoginService($this->cookie());
$service->login(42);
self::assertCount(1, $this->written);Note: the promoted-property constructor above is PHP 8 syntax. Under PHP 7.4 use a classic constructor body assigning
$this->cookie = $cookie;.
-
Passing a plain value to
$source. The source value must be the signed wire string. UseencodeCookie(); a raw'ada'has no delimiter and is discarded. -
Mismatched salt between
encodeCookie()and the manager. If the salts differ, verification fails and the cookie reads as empty — which is correct, but surprising if you did not intend it. -
Forgetting that an entry's
ttlis absolute. When building input by hand, the entry'sttlis an absolute Unix timestamp (ornull), not "seconds from now". Usetime() + Nfor valid,time() - Nfor expired. -
Mutating
$_COOKIEin tests. You do not need to — inject$sourceinstead, which keeps tests isolated and parallel-safe.
-
Configuration — the
$sourceand$writerconstructor arguments. -
Security Model — the wire format you reproduce in
encodeCookie(). -
Sending & Lifecycle — the
send()semantics your assertions pin. - API Reference — every method you might exercise.
initphp/cookies · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core Usage
Reference
Practical Guides
Migration & Help