8000 [Security] Make `PersistentToken` immutable and tell `TokenProviderIn… · symfony/symfony@7e70a3a · GitHub
[go: up one dir, main page]

Skip to content

Commit 7e70a3a

Browse files
[Security] Make PersistentToken immutable and tell TokenProviderInterface::updateToken() implementations should accept DateTimeInterface
1 parent 3471e34 commit 7e70a3a

File tree

15 files changed

+61
-37
lines changed

15 files changed

+61
-37
lines changed

UPGRADE-6.3.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ Notifier
113113
Security
114114
--------
115115

116+
* Make `PersistentToken` immutable
117+
* Deprecate accepting only `DateTime` for `TokenProviderInterface::updateToken()`, use `DateTimeInterface` instead
116118
* Deprecate passing a secret as the 2nd argument to the constructor of `Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler`
117119

118120
SecurityBundle

src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
* `class` varchar(100) NOT NULL,
4141
* `username` varchar(200) NOT NULL
4242
* );
43+
*
44+
* @final since Symfony 6.3
4345
*/
4446
class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface
4547
{
@@ -60,7 +62,7 @@ public function loadTokenBySeries(string $series): PersistentTokenInterface
6062
$row = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch(\PDO::FETCH_ASSOC);
6163

6264
if ($row) {
63-
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['last_used']));
65+
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTimeImmutable($row['last_used']));
6466
}
6567

6668
throw new TokenNotFoundException('No token found.');
@@ -82,19 +84,21 @@ public function deleteTokenBySeries(string $series)
8284
}
8385

8486
/**
87+
* @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.3
88+
*
8589
* @return void
8690
*/
8791
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
8892
{
8993
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed WHERE series=:series';
9094
$paramValues = [
9195
'value' => $tokenValue,
92-
'lastUsed' => $lastUsed,
96+
'lastUsed' => \DateTimeImmutable::createFromInterface($lastUsed),
9397
'series' => $series,
9498
];
9599
$paramTypes = [
96100
'value' => ParameterType::STRING,
97-
'lastUsed' => Types::DATETIME_MUTABLE,
101+
'lastUsed' => Types::DATETIME_IMMUTABLE,
98102
'series' => ParameterType::STRING,
99103
];
100104
if (method_exists($this->conn, 'executeStatement')) {
@@ -118,14 +122,14 @@ public function createNewToken(PersistentTokenInterface $token)
118122
'username' => $token->getUserIdentifier(),
119123
'series' => $token->getSeries(),
120124
'value' => $token->getTokenValue(),
121-
'lastUsed' => $token->getLastUsed(),
125+
'lastUsed' => \DateTimeImmutable::createFromInterface($token->getLastUsed()),
122126
];
123127
$paramTypes = [
124128
'class' => ParameterType::STRING,
125129
'username' => ParameterType::STRING,
126130
'series' => ParameterType::STRING,
127131
'value' => ParameterType::STRING,
128-
'lastUsed' => Types::DATETIME_MUTABLE,
132+
'lastUsed' => Types::DATETIME_IMMUTABLE,
129133
];
130134
if (method_exists($this->conn, 'executeStatement')) {
131135
$this->conn->executeStatement($sql, $paramValues, $paramTypes);
@@ -220,7 +224,7 @@ private function addTableToSchema(Schema $schema): void
220224
$table = $schema->createTable('rememberme_token');
221225
$table->addColumn('series', Types::STRING, ['length' => 88]);
222226
$table->addColumn('value', Types::STRING, ['length' => 88]);
223-
$table->addColumn('lastUsed', Types::DATETIME_MUTABLE);
227+
$table->addColumn('lastUsed', Types::DATETIME_IMMUTABLE);
224228
$table->addColumn('class', Types::STRING, ['length' => 100]);
225229
$table->addColumn('username', Types::STRING, ['length' => 200]);
226230
$table->setPrimaryKey(['series']);

src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public function testWithValueBound(callable $executeMethod)
126126
$stmt->bindValue(3, 5, ParameterType::INTEGER);
127127
$stmt->bindValue(4, $res = $this->getResourceFromString('mydata'), ParameterType::BINARY);
128128
$stmt->bindValue(5, ['foo', 'bar'], Types::SIMPLE_ARRAY);
129-
$stmt->bindValue(6, new \DateTime('2022-06-12 11:00:00'), Types::DATETIME_MUTABLE);
129+
$stmt->bindValue(6, new \DateTimeImmutable('2022-06-12 11:00:00'), Types::DATETIME_IMMUTABLE);
130130

131131
$executeMethod($stmt);
132132

src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function testCreateNewToken()
2626
{
2727
$provider = $this->bootstrapProvider();
2828

29-
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51'));
29+
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTimeImmutable('2013-01-26T18:23:51'));
3030
$provider->createNewToken($token);
3131

3232
$this->assertEquals($provider->loadTokenBySeries('someSeries'), $token);
@@ -44,7 +44,7 @@ public function testUpdateToken()
4444
{
4545
$provider = $this->bootstrapProvider();
4646

47-
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51'));
47+
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTimeImmutable('2013-01-26T18:23:51'));
4848
$provider->createNewToken($token);
4949
$provider->updateToken('someSeries', 'newValue', $lastUsed = new \DateTime('2014-06-26T22:03:46'));
5050
$token = $provider->loadTokenBySeries('someSeries');
@@ -56,7 +56,7 @@ public function testUpdateToken()
5656
public function testDeleteToken()
5757
{
5858
$provider = $this->bootstrapProvider();
59-
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51'));
59+
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTimeImmutable('2013-01-26T18:23:51'));
6060
$provider->createNewToken($token);
6161
$provider->deleteTokenBySeries('someSeries');
6262

@@ -73,11 +73,11 @@ public function testVerifyOutdatedTokenAfterParallelRequest()
7373
$newValue = 'newValue';
7474

7575
// setup existing token
76-
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51'));
76+
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTimeImmutable('2013-01-26T18:23:51'));
7777
$provider->createNewToken($token);
7878

7979
// new request comes in requiring remember-me auth, which updates the token
80-
$provider->updateExistingToken($token, $newValue, new \DateTime('-5 seconds'));
80+
$provider->updateExistingToken($token, $newValue, new \DateTimeImmutable('-5 seconds'));
8181
$provider->updateToken($series, $newValue, new \DateTime('-5 seconds'));
8282

8383
// parallel request comes in with the old remember-me cookie and session, which also requires reauth
@@ -98,7 +98,7 @@ public function testVerifyOutdatedTokenAfterParallelRequestFailsAfter60Seconds()
9898
$newValue = 'newValue';
9999

100100
// setup existing token
101-
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51'));
101+
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTimeImmutable('2013-01-26T18:23:51'));
102102
$provider->createNewToken($token);
103103

104104
// new request comes in requiring remember-me auth, which updates the token

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"symfony/property-access": "^5.4|^6.0",
3838
"symfony/property-info": "^5.4|^6.0",
3939
"symfony/proxy-manager-bridge": "^5.4|^6.0",
40-
"symfony/security-core": "^6.0",
40+
"symfony/security-core": "^6.3",
4141
"symfony/stopwatch": "^5.4|^6.0",
4242
"symfony/translation": "^5.4|^6.0",
4343
"symfony/uid": "^5.4|^6.0",
@@ -65,7 +65,7 @@
6565
"symfony/messenger": "<5.4",
6666
"symfony/property-info": "<5.4",
6767
"symfony/security-bundle": "<5.4",
68-
"symfony/security-core": "<6.0",
68+
"symfony/security-core": "<6.3",
6969
"symfony/validator": "<5.4"
7070
},
7171
"autoload": {

src/Symfony/Component/Security/Core/Authentication/RememberMe/InMemoryTokenProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* This class is used for testing purposes, and is not really suited for production.
1818
*
1919
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
20+
*
21+
* @final since Symfony 6.3
2022
*/
2123
class InMemoryTokenProvider implements TokenProviderInterface
2224
{
@@ -32,6 +34,8 @@ public function loadTokenBySeries(string $series): PersistentTokenInterface
3234
}
3335

3436
/**
37+
* @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.3
38+
*
3539
* @return void
3640
*/
3741
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)

src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentToken.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ final class PersistentToken implements PersistentTokenInterface
2222
private string $userIdentifier;
2323
private string $series;
2424
private string $tokenValue;
25-
private \DateTime $lastUsed;
25+
private \DateTimeImmutable $lastUsed;
2626

27-
public function __construct(string $class, string $userIdentifier, string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
27+
public function __construct(string $class, string $userIdentifier, string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed)
2828
{
2929
if (empty($class)) {
3030
throw new \InvalidArgumentException('$class must not be empty.');
@@ -43,7 +43,7 @@ public function __construct(string $class, string $userIdentifier, string $serie
4343
$this->userIdentifier = $userIdentifier;
4444
$this->series = $series;
4545
$this->tokenValue = $tokenValue;
46-
$this->lastUsed = $lastUsed;
46+
$this->lastUsed = \DateTimeImmutable::createFromInterface($lastUsed);
4747
}
4848

4949
public function getClass(): string
@@ -68,6 +68,6 @@ public function getTokenValue(): string
6868

6969
public function getLastUsed(): \DateTime
7070
{
71-
return $this->lastUsed;
71+
return \DateTime::createFromImmutable($this->lastUsed);
7272
}
7373
}

src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentTokenInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public function getTokenValue(): string;
3636

3737
/**
3838
* Returns the time the token was last used.
39+
*
40+
* Each call SHOULD return a new DateTime instance.
3941
*/
4042
public function getLastUsed(): \DateTime;
4143

src/Symfony/Component/Security/Core/Authentication/RememberMe/TokenProviderInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public function deleteTokenBySeries(string $series);
3939
/**
4040
* Updates the token according to this data.
4141
*
42+
* @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.3
43+
*
4244
* @return void
4345
*
4446
* @throws TokenNotFoundException if the token is not found

src/Symfony/Component/Security/Core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ CHANGELOG
66

77
* Add `AttributesBasedUserProviderInterface` to allow `$attributes` optional argument on `loadUserByIdentifier`
88
* Add `OidcUser` with OIDC support for `OidcUserInfoTokenHandler`
9+
* Make `PersistentToken` immutable
10+
* Deprecate accepting only `DateTime` for `TokenProviderInterface::updateToken()`, use `DateTimeInterface` instead
911

1012
6.2
1113
---

src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,23 @@ class CacheTokenVerifierTest extends TestCase
2121
public function testVerifyCurrentToken()
2222
{
2323
$verifier = new CacheTokenVerifier(new ArrayAdapter());
24-
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime());
24+
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTimeImmutable());
2525
$this->assertTrue($verifier->verifyToken($token, 'value'));
2626
}
2727

2828
public function testVerifyFailsInvalidToken()
2929
{
3030
$verifier = new CacheTokenVerifier(new ArrayAdapter());
31-
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime());
31+
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTimeImmutable());
3232
$this->assertFalse($verifier->verifyToken($token, 'wrong-value'));
3333
}
3434

3535
public function testVerifyOutdatedToken()
3636
{
3737
$verifier = new CacheTokenVerifier(new ArrayAdapter());
38-
$outdatedToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime());
39-
$newToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'newvalue', new \DateTime());
40-
$verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTime());
38+
$outdatedToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTimeImmutable());
39+
$newToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'newvalue', new \DateTimeImmutable());
40+
$verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTimeImmutable());
4141
$this->assertTrue($verifier->verifyToken($newToken, 'value'));
4242
}
4343
}

src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function testCreateNewToken()
2222
{
2323
$provider = new InMemoryTokenProvider();
2424

25-
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTime());
25+
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable());
2626
$provider->createNewToken($token);
2727

2828
$this->assertSame($provider->loadTokenBySeries('foo'), $token);
@@ -39,21 +39,21 @@ public function testUpdateToken()
3939
{
4040
$provider = new InMemoryTokenProvider();
4141

42-
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTime());
42+
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable());
4343
$provider->createNewToken($token);
4444
$provider->updateToken('foo', 'newFoo', $lastUsed = new \DateTime());
4545
$token = $provider->loadTokenBySeries('foo');
4646

4747
$this->assertEquals('newFoo', $token->getTokenValue());
48-
$this->assertSame($token->getLastUsed(), $lastUsed);
48+
$this->assertEquals($token->getLastUsed(), $lastUsed);
4949
}
5050

5151
public function testDeleteToken()
5252
{
5353
$this->expectException(TokenNotFoundException::class);
5454
$provider = new InMemoryTokenProvider();
5555

56-
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTime());
56+
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable());
5757
$provider->createNewToken($token);
5858
$provider->deleteTokenBySeries('foo');
5959
$provider->loadTokenBySeries('foo');

src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@ class PersistentTokenTest extends TestCase
1818
{
1919
public function testConstructor()
2020
{
21-
$lastUsed = new \DateTime();
21+
$lastUsed = new \DateTimeImmutable();
2222
$token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', $lastUsed);
2323

2424
$this->assertEquals('fooclass', $token->getClass());
2525
$this->assertEquals('fooname', $token->getUserIdentifier());
2626
$this->assertEquals('fooseries', $token->getSeries());
2727
$this->assertEquals('footokenvalue', $token->getTokenValue());
28-
$this->assertSame($lastUsed, $token->getLastUsed());
28+
$this->assertEquals($lastUsed, $token->getLastUsed());
29+
}
30+
31+
public function testDateTime()
32+
{
33+
$lastUsed = new \DateTime();
34+
$token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', $lastUsed);
35+
36+
$this->assertEquals($lastUsed, $token->getLastUsed());
2937
}
3038
}

src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function createRememberMeCookie(UserInterface $user): void
8888
$series = random_bytes(66);
8989
$tokenValue = strtr(base64_encode(substr($series, 33)), '+/=', '-_~');
9090
$series = strtr(base64_encode(substr($series, 0, 33)), '+/=', '-_~');
91-
$token = new PersistentToken($user::class, $user->getUserIdentifier(), $series, $tokenValue, new \DateTime());
91+
$token = new PersistentToken($user::class, $user->getUserIdentifier(), $series, $tokenValue, new \DateTimeImmutable());
9292

9393
$this->tokenProvider->createNewToken($token);
9494
$this->createCookie(RememberMeDetails::fromPersistentToken($token, time() + $this->options['lifetime']));
@@ -122,7 +122,7 @@ public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): U
122122
public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInterface $user): void
123123
{
124124
[$lastUsed, $series, $tokenValue, $class] = explode(':', $rememberMeDetails->getValue(), 4);
125-
$persistentToken = new PersistentToken($class, $rememberMeDetails->getUserIdentifier(), $series, $tokenValue, new \DateTime('@'.$lastUsed));
125+
$persistentToken = new PersistentToken($class, $rememberMeDetails->getUserIdentifier(), $series, $tokenValue, new \DateTimeImmutable('@'.$lastUsed));
126126

127127
// if a token was regenerated less than a minute ago, there is no need to regenerate it
128128
// if multiple concurrent requests reauthenticate a user we do not want to update the token several times

src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function testConsumeRememberMeCookieValid()
7878
$this->tokenProvider->expects($this->any())
7979
->method('loadTokenBySeries')
8080
->with('series1')
81-
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min')))
81+
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('-10 min')))
8282
;
8383

8484
$this->tokenProvider->expects($this->once())->method('updateToken')->with('series1');
@@ -106,7 +106,7 @@ public function testConsumeRememberMeCookieValidByValidatorWithoutUpdate()
106106
$verifier = $this->createMock(TokenVerifierInterface::class);
107107
$handler = new PersistentRememberMeHandler($this->tokenProvider, $this->userProvider, $this->requestStack, [], null, $verifier);
108108

109-
$persistentToken = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('30 seconds'));
109+
$persistentToken = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('30 seconds'));
110110

111111
$this->tokenProvider->expects($this->any())
112112
->method('loadTokenBySeries')
@@ -133,7 +133,7 @@ public function testConsumeRememberMeCookieInvalidToken()
133133
$this->tokenProvider->expects($this->any())
134134
->method('loadTokenBySeries')
135135
->with('series1')
136-
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue1', new \DateTime('-10 min')));
136+
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue1', new \DateTimeImmutable('-10 min')));
137137

138138
$this->tokenProvider->expects($this->never())->method('updateToken')->with('series1');
139139

@@ -148,7 +148,7 @@ public function testConsumeRememberMeCookieExpired()
148148
$this->tokenProvider->expects($this->any())
149149
->method('loadTokenBySeries')
150150
->with('series1')
151-
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('@'.(time() - (31536000 + 1)))));
151+
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('@'.(time() - (31536000 + 1)))));
152152

153153
$this->tokenProvider->expects($this->never())->method('updateToken')->with('series1');
154154

@@ -160,7 +160,7 @@ public function testBase64EncodedTokens()
160160
$this->tokenProvider->expects($this->any())
161161
->method('loadTokenBySeries')
162162
->with('series1')
163-
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min')))
163+
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('-10 min')))
164164
;
165165

166166
$this->tokenProvider->expects($this->once())->method('updateToken')->with('series1');

0 commit comments

Comments
 (0)
0