Skip to content

Commit a9a10a1

Browse files
committed
test: test token controller
Signed-off-by: Enrique Pérez Arnaud <enrique@cazalla.net>
1 parent 6087b34 commit a9a10a1

1 file changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Tests\unit\DAV\Controller;
11+
12+
use OC\Authentication\Token\IProvider;
13+
use OC\OCM\OCMSignatoryManager;
14+
use OCA\DAV\Controller\TokenController;
15+
use OCP\AppFramework\Http;
16+
use OCP\AppFramework\Http\DataResponse;
17+
use OCP\AppFramework\Utility\ITimeFactory;
18+
use OCP\Authentication\Exceptions\ExpiredTokenException;
19+
use OCP\Authentication\Exceptions\InvalidTokenException;
20+
use OCP\Authentication\Token\IToken;
21+
use OCP\IAppConfig;
22+
use OCP\IRequest;
23+
use OCP\Security\ISecureRandom;
24+
use OCP\Security\Signature\Exceptions\IncomingRequestException;
25+
use OCP\Security\Signature\Exceptions\SignatoryNotFoundException;
26+
use OCP\Security\Signature\Exceptions\SignatureException;
27+
use OCP\Security\Signature\Exceptions\SignatureNotFoundException;
28+
use OCP\Security\Signature\IIncomingSignedRequest;
29+
use OCP\Security\Signature\ISignatureManager;
30+
use PHPUnit\Framework\MockObject\MockObject;
31+
use Psr\Log\LoggerInterface;
32+
use Test\TestCase;
33+
34+
class TokenControllerTest extends TestCase {
35+
private IRequest&MockObject $request;
36+
private IProvider&MockObject $tokenProvider;
37+
private ISecureRandom&MockObject $random;
38+
private ITimeFactory&MockObject $timeFactory;
39+
private LoggerInterface&MockObject $logger;
40+
private ISignatureManager&MockObject $signatureManager;
41+
private OCMSignatoryManager&MockObject $signatoryManager;
42+
private IAppConfig&MockObject $appConfig;
43+
44+
private TokenController $controller;
45+
46+
protected function setUp(): void {
47+
parent::setUp();
48+
49+
$this->request = $this->createMock(IRequest::class);
50+
$this->tokenProvider = $this->createMock(IProvider::class);
51+
$this->random = $this->createMock(ISecureRandom::class);
52+
$this->timeFactory = $this->createMock(ITimeFactory::class);
53+
$this->logger = $this->createMock(LoggerInterface::class);
54+
$this->signatureManager = $this->createMock(ISignatureManager::class);
55+
$this->signatoryManager = $this->createMock(OCMSignatoryManager::class);
56+
$this->appConfig = $this->createMock(IAppConfig::class);
57+
58+
$this->controller = new TokenController(
59+
$this->request,
60+
$this->tokenProvider,
61+
$this->random,
62+
$this->timeFactory,
63+
$this->logger,
64+
$this->signatureManager,
65+
$this->signatoryManager,
66+
$this->appConfig,
67+
);
68+
}
69+
70+
public function testAccessTokenSuccess(): void {
71+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
72+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
73+
74+
$this->signatureManager->method('getIncomingSignedRequest')
75+
->with($this->signatoryManager)
76+
->willReturn($signedRequest);
77+
78+
$refreshToken = $this->createMock(IToken::class);
79+
$refreshToken->method('getType')->willReturn(IToken::PERMANENT_TOKEN);
80+
$refreshToken->method('getId')->willReturn(123);
81+
$refreshToken->method('getLoginName')->willReturn('testuser');
82+
83+
$this->tokenProvider->method('getToken')
84+
->with('valid-refresh-token')
85+
->willReturn($refreshToken);
86+
87+
$this->random->method('generate')
88+
->with(64, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS)
89+
->willReturn('generated-access-token');
90+
91+
$this->timeFactory->method('getTime')->willReturn(1000000);
92+
93+
$accessToken = $this->createMock(IToken::class);
94+
$this->tokenProvider->method('generateToken')
95+
->with(
96+
'generated-access-token',
97+
'valid-refresh-token',
98+
'testuser',
99+
null,
100+
'OCM Access Token',
101+
IToken::TEMPORARY_TOKEN,
102+
IToken::DO_NOT_REMEMBER
103+
)
104+
->willReturn($accessToken);
105+
106+
$accessToken->expects($this->once())
107+
->method('setExpires')
108+
->with(1000000 + 3600);
109+
110+
$this->tokenProvider->expects($this->once())
111+
->method('updateToken')
112+
->with($accessToken);
113+
114+
// Simulate POST body
115+
$this->simulatePostBody('grant_type=authorization_code&code=valid-refresh-token');
116+
117+
$result = $this->controller->accessToken();
118+
119+
$this->assertInstanceOf(DataResponse::class, $result);
120+
$this->assertEquals(Http::STATUS_OK, $result->getStatus());
121+
$this->assertEquals([
122+
'access_token' => 'generated-access-token',
123+
'token_type' => 'Bearer',
124+
'expires_in' => 3600,
125+
], $result->getData());
126+
}
127+
128+
public function testAccessTokenWithoutSignatureEnforcementDisabled(): void {
129+
$this->signatureManager->method('getIncomingSignedRequest')
130+
->willThrowException(new SignatureNotFoundException());
131+
132+
$this->appConfig->method('getValueBool')
133+
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, false, true)
134+
->willReturn(false);
135+
136+
$refreshToken = $this->createMock(IToken::class);
137+
$refreshToken->method('getType')->willReturn(IToken::PERMANENT_TOKEN);
138+
$refreshToken->method('getLoginName')->willReturn('testuser');
139+
140+
$this->tokenProvider->method('getToken')
141+
->willReturn($refreshToken);
142+
143+
$this->random->method('generate')->willReturn('generated-access-token');
144+
$this->timeFactory->method('getTime')->willReturn(1000000);
145+
146+
$accessToken = $this->createMock(IToken::class);
147+
$this->tokenProvider->method('generateToken')->willReturn($accessToken);
148+
149+
$this->simulatePostBody('grant_type=authorization_code&code=refresh-token');
150+
151+
$result = $this->controller->accessToken();
152+
153+
$this->assertEquals(Http::STATUS_OK, $result->getStatus());
154+
}
155+
156+
public function testAccessTokenWithoutSignatureEnforcementEnabled(): void {
157+
$this->signatureManager->method('getIncomingSignedRequest')
158+
->willThrowException(new SignatureNotFoundException());
159+
160+
$this->appConfig->method('getValueBool')
161+
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, false, true)
162+
->willReturn(true);
163+
164+
$this->simulatePostBody('grant_type=authorization_code&code=refresh-token');
165+
166+
$result = $this->controller->accessToken();
167+
168+
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $result->getStatus());
169+
$this->assertEquals(['error' => 'invalid_request'], $result->getData());
170+
}
171+
172+
public function testAccessTokenInvalidSignature(): void {
173+
$this->signatureManager->method('getIncomingSignedRequest')
174+
->willThrowException(new SignatureException('Invalid signature'));
175+
176+
$this->simulatePostBody('grant_type=authorization_code&code=refresh-token');
177+
178+
$result = $this->controller->accessToken();
179+
180+
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $result->getStatus());
181+
$this->assertEquals(['error' => 'invalid_request'], $result->getData());
182+
}
183+
184+
public function testAccessTokenUnsupportedGrantType(): void {
185+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
186+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
187+
188+
$this->signatureManager->method('getIncomingSignedRequest')
189+
->willReturn($signedRequest);
190+
191+
$this->simulatePostBody('grant_type=password&code=refresh-token');
192+
193+
$result = $this->controller->accessToken();
194+
195+
$this->assertEquals(Http::STATUS_BAD_REQUEST, $result->getStatus());
196+
$this->assertEquals(['error' => 'unsupported_grant_type'], $result->getData());
197+
}
198+
199+
public function testAccessTokenMissingGrantType(): void {
200+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
201+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
202+
203+
$this->signatureManager->method('getIncomingSignedRequest')
204+
->willReturn($signedRequest);
205+
206+
$this->simulatePostBody('code=refresh-token');
207+
208+
$result = $this->controller->accessToken();
209+
210+
$this->assertEquals(Http::STATUS_BAD_REQUEST, $result->getStatus());
211+
$this->assertEquals(['error' => 'unsupported_grant_type'], $result->getData());
212+
}
213+
214+
public function testAccessTokenMissingRefreshToken(): void {
215+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
216+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
217+
218+
$this->signatureManager->method('getIncomingSignedRequest')
219+
->willReturn($signedRequest);
220+
221+
$this->simulatePostBody('grant_type=authorization_code');
222+
223+
$result = $this->controller->accessToken();
224+
225+
$this->assertEquals(Http::STATUS_BAD_REQUEST, $result->getStatus());
226+
$this->assertEquals(['error' => 'refresh_token is required'], $result->getData());
227+
}
228+
229+
public function testAccessTokenNonPermanentToken(): void {
230+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
231+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
232+
233+
$this->signatureManager->method('getIncomingSignedRequest')
234+
->willReturn($signedRequest);
235+
236+
$refreshToken = $this->createMock(IToken::class);
237+
$refreshToken->method('getType')->willReturn(IToken::TEMPORARY_TOKEN);
238+
$refreshToken->method('getId')->willReturn(123);
239+
240+
$this->tokenProvider->method('getToken')
241+
->with('non-permanent-token')
242+
->willReturn($refreshToken);
243+
244+
$this->simulatePostBody('grant_type=authorization_code&code=non-permanent-token');
245+
246+
$result = $this->controller->accessToken();
247+
248+
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $result->getStatus());
249+
$this->assertEquals(['error' => 'invalid_grant'], $result->getData());
250+
}
251+
252+
public function testAccessTokenInvalidToken(): void {
253+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
254+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
255+
256+
$this->signatureManager->method('getIncomingSignedRequest')
257+
->willReturn($signedRequest);
258+
259+
$this->tokenProvider->method('getToken')
260+
->with('invalid-token')
261+
->willThrowException(new InvalidTokenException());
262+
263+
$this->simulatePostBody('grant_type=authorization_code&code=invalid-token');
264+
265+
$result = $this->controller->accessToken();
266+
267+
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $result->getStatus());
268+
$this->assertEquals(['error' => 'invalid_grant'], $result->getData());
269+
}
270+
271+
public function testAccessTokenExpiredToken(): void {
272+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
273+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
274+
275+
$this->signatureManager->method('getIncomingSignedRequest')
276+
->willReturn($signedRequest);
277+
278+
$this->tokenProvider->method('getToken')
279+
->with('expired-token')
280+
->willThrowException(new ExpiredTokenException($this->createMock(IToken::class)));
281+
282+
$this->simulatePostBody('grant_type=authorization_code&code=expired-token');
283+
284+
$result = $this->controller->accessToken();
285+
286+
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $result->getStatus());
287+
$this->assertEquals(['error' => 'invalid_grant'], $result->getData());
288+
}
289+
290+
public function testAccessTokenServerError(): void {
291+
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
292+
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
293+
294+
$this->signatureManager->method('getIncomingSignedRequest')
295+
->willReturn($signedRequest);
296+
297+
$this->tokenProvider->method('getToken')
298+
->willThrowException(new \RuntimeException('Database connection failed'));
299+
300+
$this->simulatePostBody('grant_type=authorization_code&code=some-token');
301+
302+
$result = $this->controller->accessToken();
303+
304+
$this->assertEquals(Http::STATUS_INTERNAL_SERVER_ERROR, $result->getStatus());
305+
$this->assertEquals(['error' => 'server_error'], $result->getData());
306+
}
307+
308+
public function testAccessTokenWithSignatoryNotFoundException(): void {
309+
$this->signatureManager->method('getIncomingSignedRequest')
310+
->willThrowException(new SignatoryNotFoundException());
311+
312+
$this->appConfig->method('getValueBool')
313+
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, false, true)
314+
->willReturn(false);
315+
316+
$refreshToken = $this->createMock(IToken::class);
317+
$refreshToken->method('getType')->willReturn(IToken::PERMANENT_TOKEN);
318+
$refreshToken->method('getLoginName')->willReturn('testuser');
319+
320+
$this->tokenProvider->method('getToken')->willReturn($refreshToken);
321+
$this->random->method('generate')->willReturn('generated-access-token');
322+
$this->timeFactory->method('getTime')->willReturn(1000000);
323+
324+
$accessToken = $this->createMock(IToken::class);
325+
$this->tokenProvider->method('generateToken')->willReturn($accessToken);
326+
327+
$this->simulatePostBody('grant_type=authorization_code&code=refresh-token');
328+
329+
$result = $this->controller->accessToken();
330+
331+
$this->assertEquals(Http::STATUS_OK, $result->getStatus());
332+
}
333+
334+
private function simulatePostBody(string $body): void {
335+
// We need to use a stream wrapper to simulate php://input
336+
stream_wrapper_unregister('php');
337+
stream_wrapper_register('php', TestPhpInputStream::class);
338+
TestPhpInputStream::$body = $body;
339+
}
340+
341+
protected function tearDown(): void {
342+
// Restore the original php stream wrapper
343+
stream_wrapper_restore('php');
344+
parent::tearDown();
345+
}
346+
}
347+
348+
/**
349+
* Helper class to simulate php://input
350+
*/
351+
class TestPhpInputStream {
352+
public static string $body = '';
353+
private int $position = 0;
354+
public mixed $context = null;
355+
356+
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
357+
if ($path === 'php://input') {
358+
$this->position = 0;
359+
return true;
360+
}
361+
return false;
362+
}
363+
364+
public function stream_read(int $count): string {
365+
$result = substr(self::$body, $this->position, $count);
366+
$this->position += strlen($result);
367+
return $result;
368+
}
369+
370+
public function stream_eof(): bool {
371+
return $this->position >= strlen(self::$body);
372+
}
373+
374+
public function stream_stat(): array {
375+
return [];
376+
}
377+
}

0 commit comments

Comments
 (0)