|
<?php |
|
|
|
namespace Feature\Routing; |
|
|
|
use App\User; |
|
use Closure; |
|
use Laravel\Passport\ClientRepository; |
|
use Tests\DatabaseTestCase; |
|
|
|
class ApiAuthTest extends DatabaseTestCase |
|
{ |
|
/** |
|
* List of all the routes that should allow access with a valid access token |
|
* @return array |
|
*/ |
|
public function accessibleRoutesProvider() |
|
{ |
|
return [ |
|
// A simple route |
|
['GET', '/api/users'], |
|
|
|
// Some setup is required, as we need a user in our database before making the request |
|
// (otherwise it'll 404). The 2nd parameter here will get overwritten by the return value of |
|
// the closure in the 3rd parameter. We still provide it for readability, both here and in |
|
// PHPUnit's output |
|
['GET', '/api/users/{userId}', function () { |
|
return '/api/users/'.$this->userId(); |
|
}], |
|
]; |
|
} |
|
|
|
/** |
|
* List of all the routes that should not allow access with a valid access token |
|
* @return array |
|
*/ |
|
public function inaccessibleRoutesProvider() |
|
{ |
|
return [ |
|
['POST', '/api/users'], |
|
['PATCH', '/api/users/{userId}', function () { |
|
return '/api/users/'.$this->userId(); |
|
}], |
|
['DELETE', '/api/users/{userId}', function () { |
|
return '/api/users/'.$this->userId(); |
|
}], |
|
['GET', '/api/users/{userId}/roles', function () { |
|
return '/api/users/'.$this->userId().'/roles'; |
|
}], |
|
]; |
|
} |
|
|
|
/** |
|
* List of all the routes |
|
* @return array |
|
*/ |
|
public function allRoutesProvider() |
|
{ |
|
return array_merge( |
|
$this->accessibleRoutesProvider(), |
|
$this->inaccessibleRoutesProvider() |
|
); |
|
} |
|
|
|
/** |
|
* Obtain a valid access token from the API and construct the correct header which should pass |
|
* any auth middleware |
|
* @return array |
|
*/ |
|
private function getValidHeaders() |
|
{ |
|
// We're using Client Credentials as an auth mechanism here (server-to-server) |
|
// Create a new client in the database so we can auth against it |
|
$clientRepository = new ClientRepository; |
|
$client = $clientRepository->create(null, 'Test', 'http://localhost'); |
|
|
|
$response = $this->json('POST', '/oauth/token', [ |
|
'grant_type' => 'client_credentials', |
|
'client_id' => $client->id, |
|
'client_secret' => $client->secret, |
|
]); |
|
|
|
$responseBody = $response->decodeResponseJson(); |
|
|
|
return ['Authorization' => 'Bearer '.$responseBody['access_token']]; |
|
} |
|
|
|
/** |
|
* Construct auth headers that should fail any auth middleware |
|
* @return array |
|
*/ |
|
public function getInvalidHeaders() |
|
{ |
|
return ['Authorization' => 'Bearer invalid-token']; |
|
} |
|
|
|
/** |
|
* @test |
|
* @dataProvider accessibleRoutesProvider |
|
*/ |
|
function can_make_an_api_call_with_valid_access_token($method, $url, Closure $dynamicUrl = null) |
|
{ |
|
$url = $this->parseUrl($url, $dynamicUrl, $dynamicUrl); |
|
|
|
$this->response = $this->withExceptionHandling()->json($method, $url, [], $this->getValidHeaders()); |
|
|
|
// Make sure we get a response that shows we successfully passed authentication |
|
$this->assertContains($this->response->getStatusCode(), [200, 201, 204, 400, 422]); |
|
} |
|
|
|
/** |
|
* @test |
|
* @dataProvider inaccessibleRoutesProvider |
|
*/ |
|
function cannot_make_an_api_call_with_valid_access_token($method, $url, Closure $dynamicUrl = null) |
|
{ |
|
$url = $this->parseUrl($url, $dynamicUrl); |
|
|
|
$this->response = $this->withExceptionHandling()->json($method, $url, [], $this->getValidHeaders()); |
|
|
|
$this->assertUnauthorised(); |
|
} |
|
|
|
/** |
|
* @test |
|
* @dataProvider allRoutesProvider |
|
*/ |
|
function cannot_make_an_api_call_with_invalid_access_token($method, $url, Closure $dynamicUrl = null) |
|
{ |
|
$url = $this->parseUrl($url, $dynamicUrl); |
|
|
|
$this->response = $this->withExceptionHandling()->json($method, $url, [], $this->getInvalidHeaders()); |
|
|
|
$this->assertUnauthorised(); |
|
} |
|
|
|
/** |
|
* Some endpoints need to run some set-up before we can call them, |
|
* for example setting up a product ID before updating that product over the API |
|
* Passing in a $dynamicUrl will overwrite the $url and run it before attempting to |
|
* hit the API. |
|
* @param string $url |
|
* @param Closure|null $dynamicUrl if provided, run and overwrite the $url |
|
* @return string The final URL to call |
|
*/ |
|
private function parseUrl($url, $dynamicUrl) |
|
{ |
|
return !empty($dynamicUrl) ? $dynamicUrl() : $url; |
|
} |
|
|
|
/** |
|
* Helper function to assert that the response was "401 Unauthorized" |
|
*/ |
|
private function assertUnauthorised() |
|
{ |
|
$this->response->assertStatus(401); |
|
$this->response->assertExactJson([ |
|
'errors' => [ |
|
[ |
|
'code' => 'ERR-401', |
|
'title' => 'AuthenticationException', |
|
'details' => 'Unauthenticated.', |
|
], |
|
], |
|
]); |
|
} |
|
|
|
/** |
|
* Create a user in the database and get their ID |
|
* @return integer |
|
*/ |
|
private function userId() |
|
{ |
|
return factory(User::class)->create()->id; |
|
} |
|
} |