diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index 82592c2..fb5573a 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -1,5 +1,6 @@ request->session()->set('oauth_state', $state); - $oAuth = new GoogleOAuth(); + $oAuth = new GoogleOAuth(new Request()); $url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()); return new Redirect($url, IRedirect::TEMPORARY); @@ -147,7 +148,7 @@ class LoginController return new HtmlContent('login/google_login', $data); } - $oAuth = new GoogleOAuth(); + $oAuth = new GoogleOAuth(new Request()); $tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()); if (!isset($tokenData['id_token'])) { diff --git a/src/Http/Request.php b/src/Http/Request.php index 2e5426a..9f1a05b 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -1,11 +1,10 @@ url = $url; $this->method = $method; } + public function setUrl(string $url): void + { + $this->url = $url; + } + + public function setMethod(int $method): void + { + $this->method = $method; + } + public function setQuery($query) { if (is_string($query)) { @@ -34,7 +43,7 @@ class Request $this->headers = array_merge($this->headers, $headers); } - public function send(): Response + public function send(): IResponse { $ch = curl_init(); diff --git a/src/Http/Response.php b/src/Http/Response.php index 9bee447..65958b0 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -1,6 +1,8 @@ request = $request; + } + public function getDialogUrl(string $state, string $redirectUrl): string { $oauthParams = [ @@ -32,9 +39,10 @@ class GoogleOAuth 'grant_type' => 'authorization_code', ]; - $request = new Request(self::$tokenUrlBase, Request::HTTP_POST); - $request->setQuery($tokenParams); - $response = $request->send(); + $this->request->setUrl(self::$tokenUrlBase); + $this->request->setMethod(IRequest::HTTP_POST); + $this->request->setQuery($tokenParams); + $response = $this->request->send(); return json_decode($response->getBody(), true); } diff --git a/tests/OAuth/GoogleOAuthTest.php b/tests/OAuth/GoogleOAuthTest.php new file mode 100644 index 0000000..a40fc66 --- /dev/null +++ b/tests/OAuth/GoogleOAuthTest.php @@ -0,0 +1,90 @@ +getMockBuilder(IRequest::class) + ->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send']) + ->getMock(); + $googleOAuth = new GoogleOAuth($requestMock); + + $dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl); + $dialogUrlParsed = explode('?', $dialogUrl); + + $this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]); + + parse_str($dialogUrlParsed[1], $dialogUrlQueryParams); + + $expectedQueryParams = [ + 'response_type' => 'code', + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'scope' => 'openid email', + 'redirect_uri' => $redirectUrl, + 'state' => $state, + 'nonce' => hash('sha256', random_bytes(10) . microtime()), + ]; + + $this->assertEquals($expectedQueryParams['response_type'], $dialogUrlQueryParams['response_type']); + $this->assertEquals($expectedQueryParams['client_id'], $dialogUrlQueryParams['client_id']); + $this->assertEquals($expectedQueryParams['scope'], $dialogUrlQueryParams['scope']); + $this->assertEquals($expectedQueryParams['redirect_uri'], $dialogUrlQueryParams['redirect_uri']); + $this->assertEquals($expectedQueryParams['state'], $dialogUrlQueryParams['state']); + $this->assertMatchesRegularExpression('/^[a-f0-9]{64}$/', $dialogUrlQueryParams['nonce']); + } + + public function testCanRequestToken(): void + { + $_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'abc'; + $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'] = 'xxx'; + $code = 'code_from_google'; + $redirectUrl = 'http://example.com/oauth'; + + $requestMock = $this->getMockBuilder(IRequest::class) + ->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send']) + ->getMock(); + $responseMock = $this->getMockBuilder(IResponse::class) + ->setMethods(['getBody', 'getHeaders']) + ->getMock(); + $googleOAuth = new GoogleOAuth($requestMock); + + $expectedQueryParams = [ + 'code' => $code, + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'], + 'redirect_uri' => $redirectUrl, + 'grant_type' => 'authorization_code', + ]; + + $requestMock->expects($this->once()) + ->method('setUrl') + ->with($this->equalTo('https://oauth2.googleapis.com/token')); + $requestMock->expects($this->once()) + ->method('setMethod') + ->with($this->equalTo(IRequest::HTTP_POST)); + $requestMock->expects($this->once()) + ->method('setQuery') + ->with($this->equalTo($expectedQueryParams)); + $requestMock->expects($this->once()) + ->method('send') + ->will($this->returnValue($responseMock)); + $responseMock->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('{"test":"json"}')); + + $token = $googleOAuth->getToken($code, $redirectUrl); + + $this->assertEquals(['test' => 'json'], $token); + } +} diff --git a/tests/PersistentData/Model/ModelTest.php b/tests/PersistentData/Model/ModelTest.php new file mode 100644 index 0000000..6e0b042 --- /dev/null +++ b/tests/PersistentData/Model/ModelTest.php @@ -0,0 +1,89 @@ + OtherModel::class]; + + private string $name; + + private bool $valid; + + public function setName(string $name): void + { + $this->name = $name; + } + + public function setValid(bool $valid): void + { + $this->valid = $valid; + } + + public function getName(): string + { + return $this->name; + } + + public function getValid(): bool + { + return $this->valid; + } +} + +final class ModelTest extends TestCase +{ + public function testCanReturnTable(): void + { + $this->assertEquals('test_table', DummyModel::getTable()); + } + + public function testCanReturnFields(): void + { + $this->assertEquals(['id', 'name', 'valid'], DummyModel::getFields()); + } + + public function testCanReturnRelations(): void + { + $this->assertEquals(['other_model' => OtherModel::class], DummyModel::getRelations()); + } + + public function testCanBeConvertedToArray(): void + { + $model = new DummyModel(); + $model->setId(123); + $model->setName('John'); + $model->setValid(true); + + $this->assertEquals([ + 'id' => 123, + 'name' => 'John', + 'valid' => true + ], $model->toArray()); + } + + public function testCanSaveAndResetSnapshot(): void + { + $model = new DummyModel(); + $model->setId(123); + $model->setName('John'); + $model->setValid(true); + + $model->saveSnapshot(); + + $this->assertEquals([ + 'id' => 123, + 'name' => 'John', + 'valid' => true + ], $model->getSnapshot()); + + $model->resetSnapshot(); + + $this->assertEquals([], $model->getSnapshot()); + } +} diff --git a/tests/Util/JwtParserTest.php b/tests/Util/JwtParserTest.php new file mode 100644 index 0000000..468f93b --- /dev/null +++ b/tests/Util/JwtParserTest.php @@ -0,0 +1,51 @@ +jwtParser = new JwtParser( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + ); + } + + public function testSettingTokenIsTheSameAsCreatingWithToken(): void + { + $jwtParser2 = new JwtParser(); + $jwtParser2->setToken( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + ); + + $this->assertEquals($this->jwtParser, $jwtParser2); + } + + public function testCanParseTokenHeader(): void + { + $this->assertEquals([ + 'alg' => 'HS256', + 'typ' => 'JWT' + ], $this->jwtParser->getHeader()); + } + + public function testCanParseTokenPayload(): void + { + $this->assertEquals([ + 'sub' => '1234567890', + 'name' => 'John Doe', + 'iat' => 1516239022 + ], $this->jwtParser->getPayload()); + } + + public function testCanParseTokenSignature(): void + { + $this->assertEquals( + '49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397', + bin2hex($this->jwtParser->getSignature()) + ); + } +}