Skip to content

Commit 741f5c5

Browse files
authored
Cookie expire date can be integer or float (#67)
Refactor cookie validation into internal class to reduce duplication
1 parent d66df8c commit 741f5c5

File tree

5 files changed

+258
-66
lines changed

5 files changed

+258
-66
lines changed

src/Browser/StorageState.php

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
namespace Playwright\Browser;
1616

17+
use Playwright\Cookie;
1718
use Playwright\Exception\RuntimeException;
1819

1920
/**
@@ -22,8 +23,8 @@
2223
final readonly class StorageState
2324
{
2425
/**
25-
* @param array<array{name: string, value: string, domain: string, path: string, expires: int, httpOnly: bool, secure: bool, sameSite: 'Strict'|'Lax'|'None'}> $cookies
26-
* @param array<array{origin: string, localStorage?: array<array{name: string, value: string}>}> $origins
26+
* @param array<array{name: string, value: string, domain: string, path: string, expires: int|float, httpOnly: bool, secure: bool, sameSite: 'Strict'|'Lax'|'None'}> $cookies
27+
* @param array<array{origin: string, localStorage?: array<array{name: string, value: string}>}> $origins
2728
*/
2829
public function __construct(
2930
public array $cookies = [],
@@ -140,7 +141,7 @@ public function getOriginCount(): int
140141
}
141142

142143
/**
143-
* @return array<array{name: string, value: string, domain: string, path: string, expires: int, httpOnly: bool, secure: bool, sameSite: 'Strict'|'Lax'|'None'}>
144+
* @return array<array{name: string, value: string, domain: string, path: string, expires: int|float, httpOnly: bool, secure: bool, sameSite: 'Strict'|'Lax'|'None'}>
144145
*/
145146
public function getCookiesForDomain(string $domain): array
146147
{
@@ -164,7 +165,7 @@ public function getLocalStorageForOrigin(string $origin): array
164165
/**
165166
* @param array<mixed, mixed> $cookies
166167
*
167-
* @return array<array{name: string, value: string, domain: string, path: string, expires: int, httpOnly: bool, secure: bool, sameSite: 'Lax'|'None'|'Strict'}>
168+
* @return array<array{name: string, value: string, domain: string, path: string, expires: int|float, httpOnly: bool, secure: bool, sameSite: 'Lax'|'None'|'Strict'}>
168169
*/
169170
private static function validateCookies(array $cookies): array
170171
{
@@ -174,38 +175,7 @@ private static function validateCookies(array $cookies): array
174175
throw new \InvalidArgumentException('Invalid cookie data structure');
175176
}
176177

177-
$name = $cookie['name'] ?? null;
178-
$value = $cookie['value'] ?? null;
179-
$domain = $cookie['domain'] ?? null;
180-
$path = $cookie['path'] ?? null;
181-
$expires = $cookie['expires'] ?? null;
182-
$httpOnly = $cookie['httpOnly'] ?? null;
183-
$secure = $cookie['secure'] ?? null;
184-
$sameSite = $cookie['sameSite'] ?? null;
185-
186-
if (!is_string($name)
187-
|| !is_string($value)
188-
|| !is_string($domain)
189-
|| !is_string($path)
190-
|| !is_int($expires)
191-
|| !is_bool($httpOnly)
192-
|| !is_bool($secure)
193-
|| !is_string($sameSite)
194-
|| !in_array($sameSite, ['Lax', 'None', 'Strict'], true)
195-
) {
196-
throw new \InvalidArgumentException('Invalid cookie fields');
197-
}
198-
199-
$result[] = [
200-
'name' => $name,
201-
'value' => $value,
202-
'domain' => $domain,
203-
'path' => $path,
204-
'expires' => $expires,
205-
'httpOnly' => $httpOnly,
206-
'secure' => $secure,
207-
'sameSite' => $sameSite,
208-
];
178+
$result[] = Cookie::fromArray($cookie)->toArray();
209179
}
210180

211181
return $result;

src/Cookie.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the community-maintained Playwright PHP project.
7+
* It is not affiliated with or endorsed by Microsoft.
8+
*
9+
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
10+
*
11+
* For the full copyright and license information, please view the LICENSE
12+
* file that was distributed with this source code.
13+
*/
14+
15+
namespace Playwright;
16+
17+
/**
18+
* @internal
19+
*/
20+
final class Cookie
21+
{
22+
/**
23+
* @param 'Strict'|'Lax'|'None' $sameSite
24+
*/
25+
private function __construct(
26+
public string $name,
27+
public string $value,
28+
public string $domain,
29+
public string $path,
30+
public int|float $expires,
31+
public bool $httpOnly,
32+
public bool $secure,
33+
public string $sameSite,
34+
) {
35+
}
36+
37+
/**
38+
* @param array<mixed, mixed> $cookie
39+
*/
40+
public static function fromArray(array $cookie): self
41+
{
42+
$sameSite = $cookie['sameSite'] ?? null;
43+
44+
if (!is_string($cookie['name'] ?? null)
45+
|| !is_string($cookie['value'] ?? null)
46+
|| !is_string($cookie['domain'] ?? null)
47+
|| !is_string($cookie['path'] ?? null)
48+
|| (!is_int($cookie['expires'] ?? null) && !is_float($cookie['expires'] ?? null))
49+
|| !is_bool($cookie['httpOnly'] ?? null)
50+
|| !is_bool($cookie['secure'] ?? null)
51+
|| !in_array($sameSite, ['Lax', 'None', 'Strict'], true)
52+
) {
53+
throw new \InvalidArgumentException('Invalid cookie fields');
54+
}
55+
56+
return new self(
57+
name: $cookie['name'],
58+
value: $cookie['value'],
59+
domain: $cookie['domain'],
60+
path: $cookie['path'],
61+
expires: $cookie['expires'],
62+
httpOnly: $cookie['httpOnly'],
63+
secure: $cookie['secure'],
64+
sameSite: $sameSite,
65+
);
66+
}
67+
68+
/**
69+
* @return array{name: string, value: string, domain: string, path: string, expires: int|float, httpOnly: bool, secure: bool, sameSite: 'Strict'|'Lax'|'None'}
70+
*/
71+
public function toArray(): array
72+
{
73+
return [
74+
'name' => $this->name,
75+
'value' => $this->value,
76+
'domain' => $this->domain,
77+
'path' => $this->path,
78+
'expires' => $this->expires,
79+
'httpOnly' => $this->httpOnly,
80+
'secure' => $this->secure,
81+
'sameSite' => $this->sameSite,
82+
];
83+
}
84+
}

src/Page/Page.php

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Playwright\Clock\NullClock;
2121
use Playwright\Configuration\PlaywrightConfig;
2222
use Playwright\Console\ConsoleMessage;
23+
use Playwright\Cookie;
2324
use Playwright\Dialog\Dialog;
2425
use Playwright\Event\EventDispatcherInterface;
2526
use Playwright\Exception\NetworkException;
@@ -673,6 +674,11 @@ public function context(): BrowserContextInterface
673674
return $this->context;
674675
}
675676

677+
/**
678+
* @param array<string>|null $urls
679+
*
680+
* @return array<array{name: string, value: string, domain: string, path: string, expires: int|float, httpOnly: bool, secure: bool, sameSite: 'Strict'|'Lax'|'None'}>
681+
*/
676682
public function cookies(?array $urls = null): array
677683
{
678684
$response = $this->sendCommand('cookies', ['urls' => $urls]);
@@ -688,38 +694,11 @@ public function cookies(?array $urls = null): array
688694
continue;
689695
}
690696

691-
$name = $cookie['name'] ?? null;
692-
$value = $cookie['value'] ?? null;
693-
$domain = $cookie['domain'] ?? null;
694-
$path = $cookie['path'] ?? null;
695-
$expires = $cookie['expires'] ?? null;
696-
$httpOnly = $cookie['httpOnly'] ?? null;
697-
$secure = $cookie['secure'] ?? null;
698-
$sameSite = $cookie['sameSite'] ?? null;
699-
700-
if (!is_string($name)
701-
|| !is_string($value)
702-
|| !is_string($domain)
703-
|| !is_string($path)
704-
|| !is_int($expires)
705-
|| !is_bool($httpOnly)
706-
|| !is_bool($secure)
707-
|| !is_string($sameSite)
708-
|| !in_array($sameSite, ['Lax', 'None', 'Strict'], true)
709-
) {
697+
try {
698+
$validatedCookies[] = Cookie::fromArray($cookie)->toArray();
699+
} catch (\InvalidArgumentException) {
710700
continue;
711701
}
712-
713-
$validatedCookies[] = [
714-
'name' => $name,
715-
'value' => $value,
716-
'domain' => $domain,
717-
'path' => $path,
718-
'expires' => $expires,
719-
'httpOnly' => $httpOnly,
720-
'secure' => $secure,
721-
'sameSite' => $sameSite,
722-
];
723702
}
724703

725704
return $validatedCookies;

tests/Unit/Browser/StorageStateTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ public function itCreatesStorageStateWithData(): void
8282
$this->assertEquals(1, $storageState->getOriginCount());
8383
}
8484

85+
#[Test]
86+
public function itCreatesStorageStateWithCookieDataWithFloatExpires(): void
87+
{
88+
$cookies = [
89+
[
90+
'name' => 'session_id',
91+
'value' => 'abc123',
92+
'domain' => 'example.com',
93+
'path' => '/',
94+
'expires' => 12345678.90,
95+
'httpOnly' => true,
96+
'secure' => true,
97+
'sameSite' => 'Strict',
98+
],
99+
];
100+
101+
$storageState = StorageState::fromArray(['cookies' => $cookies]);
102+
103+
$this->assertEquals($cookies, $storageState->cookies);
104+
$this->assertFalse($storageState->isEmpty());
105+
$this->assertEquals(1, $storageState->getCookieCount());
106+
}
107+
108+
#[Test]
109+
public function itThrowsExceptionForInvalidCookieData(): void
110+
{
111+
$this->expectException(\InvalidArgumentException::class);
112+
$this->expectExceptionMessage('Invalid cookie fields');
113+
114+
StorageState::fromArray(['cookies' => [['invalid' => 'data']]]);
115+
}
116+
117+
#[Test]
118+
public function itThrowsExceptionForInvalidCookies(): void
119+
{
120+
$this->expectException(\InvalidArgumentException::class);
121+
$this->expectExceptionMessage('Invalid cookie data structure');
122+
123+
StorageState::fromArray(['cookies' => ['invalid' => 'data']]);
124+
}
125+
85126
#[Test]
86127
public function itCreatesFromJson(): void
87128
{

tests/Unit/CookieTest.php

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the community-maintained Playwright PHP project.
7+
* It is not affiliated with or endorsed by Microsoft.
8+
*
9+
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
10+
*
11+
* For the full copyright and license information, please view the LICENSE
12+
* file that was distributed with this source code.
13+
*/
14+
15+
namespace Playwright\Tests\Unit;
16+
17+
use PHPUnit\Framework\Attributes\CoversClass;
18+
use PHPUnit\Framework\Attributes\DataProvider;
19+
use PHPUnit\Framework\Attributes\Test;
20+
use PHPUnit\Framework\TestCase;
21+
use Playwright\Cookie;
22+
23+
#[CoversClass(Cookie::class)]
24+
final class CookieTest extends TestCase
25+
{
26+
#[DataProvider('validCookieProvider')]
27+
#[Test]
28+
public function itCreatesFromArrayValidCookie(array $cookieData, int|float $expires): void
29+
{
30+
$cookie = Cookie::fromArray($cookieData);
31+
32+
$this->assertSame('test', $cookie->name);
33+
$this->assertSame('value', $cookie->value);
34+
$this->assertSame('example.com', $cookie->domain);
35+
$this->assertSame('/', $cookie->path);
36+
$this->assertSame($expires, $cookie->expires);
37+
$this->assertTrue($cookie->httpOnly);
38+
$this->assertTrue($cookie->secure);
39+
$this->assertSame('Strict', $cookie->sameSite);
40+
}
41+
42+
public static function validCookieProvider(): iterable
43+
{
44+
yield 'expires with int' => [
45+
[
46+
'name' => 'test',
47+
'value' => 'value',
48+
'domain' => 'example.com',
49+
'path' => '/',
50+
'expires' => 3600,
51+
'httpOnly' => true,
52+
'secure' => true,
53+
'sameSite' => 'Strict',
54+
],
55+
3600,
56+
];
57+
58+
yield 'expires with float' => [
59+
[
60+
'name' => 'test',
61+
'value' => 'value',
62+
'domain' => 'example.com',
63+
'path' => '/',
64+
'expires' => 3600.5,
65+
'httpOnly' => true,
66+
'secure' => true,
67+
'sameSite' => 'Strict',
68+
],
69+
3600.5,
70+
];
71+
}
72+
73+
#[DataProvider('invalidCookieProvider')]
74+
#[Test]
75+
public function itFailsToCreateFromArrayInvalidCookie(array $cookieData): void
76+
{
77+
$this->expectException(\InvalidArgumentException::class);
78+
79+
Cookie::fromArray($cookieData);
80+
}
81+
82+
public static function invalidCookieProvider(): iterable
83+
{
84+
yield 'Invalid cookie data structure' => [['invalid' => 'data']];
85+
yield 'Invalid cookie expires as random string' => [['name' => 'test', 'value' => 'value', 'domain' => 'example.com', 'path' => '/', 'expires' => 'invalid', 'httpOnly' => true, 'secure' => true, 'sameSite' => 'Strict']];
86+
yield 'Invalid cookie expires as string' => [['name' => 'test', 'value' => 'value', 'domain' => 'example.com', 'path' => '/', 'expires' => '123', 'httpOnly' => true, 'secure' => true, 'sameSite' => 'Strict']];
87+
yield 'Invalid cookie sameSite' => [['name' => 'test', 'value' => 'value', 'domain' => 'example.com', 'path' => '/', 'expires' => 123, 'httpOnly' => true, 'secure' => true, 'sameSite' => 'Random']];
88+
}
89+
90+
#[Test]
91+
public function itReturnsValidCookieArray(): void
92+
{
93+
$cookie = Cookie::fromArray([
94+
'name' => 'test',
95+
'value' => 'value',
96+
'domain' => 'example.com',
97+
'path' => '/',
98+
'expires' => 3600,
99+
'httpOnly' => true,
100+
'secure' => true,
101+
'sameSite' => 'Strict',
102+
]);
103+
104+
$this->assertSame(
105+
[
106+
'name' => 'test',
107+
'value' => 'value',
108+
'domain' => 'example.com',
109+
'path' => '/',
110+
'expires' => 3600,
111+
'httpOnly' => true,
112+
'secure' => true,
113+
'sameSite' => 'Strict',
114+
],
115+
$cookie->toArray()
116+
);
117+
}
118+
}

0 commit comments

Comments
 (0)