lightning_api-8.x-4.x-dev/tests/src/Functional/ApiTest.php

tests/src/Functional/ApiTest.php
<?php

namespace Drupal\Tests\lightning_api\Functional;

use Drupal\Component\Serialization\Json;
use Drupal\consumers\Entity\Consumer;
use Drupal\Core\Url;
use Drupal\lightning_api\Form\OAuthKeyForm;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\simple_oauth\Functional\RequestHelperTrait;
use GuzzleHttp\Exception\ClientException;
use Psr\Http\Message\ResponseInterface;

/**
 * Tests that OAuth and JSON:API authenticate and authorize entity operations.
 *
 * @group lightning_api
 * @group headless
 * @group orca_public
 *
 * @requires module simple_oauth
 */
class ApiTest extends BrowserTestBase {

  use RequestHelperTrait;

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'lightning_api',
    'node',
    'path',
    'simple_oauth',
    'simple_oauth_test',
    'taxonomy',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    // Allow writing via JSON:API.
    $this->config('jsonapi.settings')->set('read_only', FALSE)->save();

    // Log in as an administrator so that we can generate security keys for
    // OAuth.
    $account = $this->drupalCreateUser([], NULL, TRUE);
    $this->drupalLogin($account);

    // Visiting Simple OAuth's token settings form should display a warning
    // that no keys exist yet, and invite the user to use our generator form.
    $this->drupalGet('/admin/config/people/simple_oauth');
    $assert_session = $this->assertSession();
    $assert_session->pageTextContains('You may wish to generate a key pair for OAuth authentication.');
    $this->clickLink('generate a key pair');

    $page = $this->getSession()->getPage();
    $page->fillField('Destination', $this->container->get('file_system')->getTempDirectory());
    $page->fillField('Private key name', 'private.key');
    $page->fillField('Public key name', 'public.key');
    $conf = getenv('OPENSSL_CONF');
    if ($conf) {
      $page->fillField('OpenSSL configuration file', $conf);
    }
    $page->pressButton('Generate keys');
    $assert_session->pageTextContains('A key pair was generated successfully.');

    // Check that the keys actually exist and have the correct permissions.
    $config = $this->config('simple_oauth.settings');
    foreach (['public', 'private'] as $which) {
      $path = $config->get("{$which}_key");
      $this->assertNotEmpty($path);
      $this->assertFileExists($path);
      $this->assertSame(OAuthKeyForm::KEY_PERMISSIONS, fileperms($path) & 0777);
    }

    $this->drupalLogout();
  }

  /**
   * {@inheritdoc}
   */
  protected function createContentType(array $values = []) {
    $node_type = $this->drupalCreateContentType($values);
    // The router needs to be rebuilt in order for the new content type to be
    // available to JSON:API.
    $this->container->get('router.builder')->rebuild();
    return $node_type;
  }

  /**
   * Creates an API user with all privileges for a single content type.
   *
   * @param string $node_type
   *   The content type ID.
   *
   * @return string
   *   The API access token.
   */
  private function getCreator($node_type) {
    return $this->createApiUser([
      "access content",
      "bypass node access",
      "create $node_type content",
      "create url aliases",
      "delete $node_type revisions",
      "edit any $node_type content",
      "edit own $node_type content",
      "revert $node_type revisions",
      "view all revisions",
      "view own unpublished content",
      "view $node_type revisions",
    ]);
  }

  /**
   * Creates a user account with privileged API access.
   *
   * @see ::createUser()
   *
   * @return string
   *   The user's access token.
   */
  private function createApiUser(array $permissions = [], $name = NULL, $admin = FALSE) {
    // We should not be logged in right now.
    $this->assertEmpty($this->loggedInUser);

    $permissions[] = 'grant simple_oauth codes';
    $account = $this->createUser($permissions, $name, $admin);
    $this->drupalLogin($account);

    $roles = $account->getRoles(TRUE);
    $secret = $this->randomString(32);

    $redirect_url = Url::fromRoute('oauth2_token.test_token')
      ->setAbsolute()
      ->toString();

    $client = Consumer::create([
      'label' => 'API Test Client',
      'secret' => $secret,
      'confidential' => TRUE,
      'user_id' => $account->id(),
      'roles' => reset($roles),
      'client_id' => $this->randomMachineName(16),
      'grant_types' => ['authorization_code'],
      'redirect' => $redirect_url,
    ]);
    $client->save();

    // Ask for an access code, which we can swap for a token.
    $url = Url::fromRoute('oauth2_token.authorize');
    $this->drupalGet($url, [
      'query' => [
        'response_type' => 'code',
        'client_id' => $client->getClientId(),
        'client_secret' => $secret,
        'redirect_uri' => $redirect_url,
      ],
    ]);
    $session = $this->getSession();
    $session->getPage()->pressButton('Grant');
    // We should now have an authorization code.
    $assert_session = $this->assertSession();
    $assert_session->statusCodeEquals(200);
    $parsed_url = parse_url($session->getCurrentUrl());
    $this->assertArrayHasKey('query', $parsed_url);
    $query = [];
    parse_str($parsed_url['query'], $query);
    $this->assertNotEmpty($query['code']);

    // Exchange the authorization code for a shiny token.
    $url = Url::fromRoute('oauth2_token.token');
    $response = $this->post($url, [
      'grant_type' => 'authorization_code',
      'client_id' => $client->getClientId(),
      'client_secret' => $secret,
      'code' => $query['code'],
      'scope' => implode(' ', $roles),
      'redirect_uri' => $redirect_url,
    ]);
    $this->assertGreaterThanOrEqual(200, $response->getStatusCode());
    $this->assertLessThan(300, $response->getStatusCode());
    $body = $this->decodeResponse($response);
    $this->assertNotEmpty($body['access_token']);

    return $body['access_token'];
  }

  /**
   * Tests create, read, and update of content entities via the API.
   */
  public function testEntities() {
    $access_token = $this->createApiUser(['administer taxonomy'], NULL, TRUE);

    // Create a taxonomy vocabulary. This cannot currently be done over the API
    // because jsonapi doesn't really support it, and will not be able to
    // properly support it until config entities can be internally validated
    // and access controlled outside of the UI.
    $vocabulary = Vocabulary::create([
      'name' => "I'm a vocab",
      'vid' => 'im_a_vocab',
      'status' => TRUE,
    ]);
    $vocabulary->save();

    $endpoint = '/jsonapi/taxonomy_vocabulary/taxonomy_vocabulary/' . $vocabulary->uuid();

    // Read the newly created vocabulary.
    $response = $this->request($endpoint, 'get', $access_token);
    $body = $this->decodeResponse($response);
    $this->assertSame($vocabulary->label(), $body['data']['attributes']['name']);

    $vocabulary->set('name', 'Still a vocab, just a different title');
    $vocabulary->save();
    // The router needs to be rebuilt in order for the new vocabulary to be
    // available to JSON:API.
    $this->container->get('router.builder')->rebuild();

    // Read the updated vocabulary.
    $response = $this->request($endpoint, 'get', $access_token);
    $body = $this->decodeResponse($response);
    $this->assertSame($vocabulary->label(), $body['data']['attributes']['name']);

    // Assert that the newly created vocabulary's endpoint is reachable.
    $response = $this->request('/jsonapi/taxonomy_term/im_a_vocab');
    $this->assertSame(200, $response->getStatusCode());

    $name = 'zebra';
    $term_uuid = $this->container->get('uuid')->generate();
    $endpoint = '/jsonapi/taxonomy_term/im_a_vocab/' . $term_uuid;

    // Create a taxonomy term (content entity).
    $this->request('/jsonapi/taxonomy_term/im_a_vocab', 'post', $access_token, [
      'data' => [
        'type' => 'taxonomy_term--im_a_vocab',
        'id' => $term_uuid,
        'attributes' => [
          'name' => $name,
          'uuid' => $term_uuid,
        ],
        'relationships' => [
          'vid' => [
            'data' => [
              'type' => 'taxonomy_vocabulary--taxonomy_vocabulary',
              'id' => $vocabulary->uuid(),
            ],
          ],
        ],
      ],
    ]);

    // Read the taxonomy term.
    $response = $this->request($endpoint, 'get', $access_token);
    $body = $this->decodeResponse($response);
    $this->assertSame($name, $body['data']['attributes']['name']);

    $new_name = 'squid';

    // Update the taxonomy term.
    $this->request($endpoint, 'patch', $access_token, [
      'data' => [
        'type' => 'taxonomy_term--im_a_vocab',
        'id' => $term_uuid,
        'attributes' => [
          'name' => $new_name,
        ],
      ],
    ]);

    // Read the updated taxonomy term.
    $response = $this->request($endpoint, 'get', $access_token);
    $body = $this->decodeResponse($response);
    $this->assertSame($new_name, $body['data']['attributes']['name']);
  }

  /**
   * Tests Getting data as anon and authenticated user.
   */
  public function testAllowed() {
    $this->createContentType(['type' => 'page']);
    // Create some sample content for testing. One published and one unpublished
    // basic page.
    $published_node = $this->drupalCreateNode();
    $unpublished_node = $published_node->createDuplicate()->setUnpublished();
    $unpublished_node->save();

    // Get data that is available anonymously.
    $response = $this->request('/jsonapi/node/page/' . $published_node->uuid());
    $this->assertSame(200, $response->getStatusCode());
    $body = $this->decodeResponse($response);
    $this->assertSame($published_node->getTitle(), $body['data']['attributes']['title']);

    // Get data that requires authentication.
    $access_token = $this->getCreator('page');
    $response = $this->request('/jsonapi/node/page/' . $unpublished_node->uuid(), 'get', $access_token);
    $this->assertSame(200, $response->getStatusCode());
    $body = $this->decodeResponse($response);
    $this->assertSame($unpublished_node->getTitle(), $body['data']['attributes']['title']);

    // Post new content that requires authentication.
    $count = (int) \Drupal::entityQuery('node')->accessCheck(TRUE)->count()->execute();
    $this->request('/jsonapi/node/page', 'post', $access_token, [
      'data' => [
        'type' => 'node--page',
        'attributes' => [
          'title' => 'With my own two hands',
        ],
      ],
    ]);
    $this->assertSame(++$count, (int) \Drupal::entityQuery('node')->accessCheck(TRUE)->count()->execute());
  }

  /**
   * Tests access to unauthorized data is denied, regardless of authentication.
   */
  public function testForbidden() {
    $this->createContentType(['type' => 'page']);

    // Cannot get unauthorized data (not in role/scope) even when authenticated.
    $response = $this->request('/jsonapi/user_role/user_role', 'get', $this->getCreator('page'));
    $body = $this->decodeResponse($response);
    $this->assertSame('array', gettype($body['meta']['omitted']['links']));
    $this->assertNotEmpty($body['meta']['omitted']['links']);
    unset($body['meta']['omitted']['links']['help']);

    foreach ($body['meta']['omitted']['links'] as $link) {
      // This user/client should not have access to any of the roles' data.
      $this->assertSame(
        "The current user is not allowed to GET the selected resource. The 'administer permissions' permission is required.",
        $link['meta']['detail']
      );
    }

    // Cannot get unauthorized data anonymously.
    $unpublished_node = $this->drupalCreateNode()->setUnpublished();
    $unpublished_node->save();
    $url = $this->buildUrl('/jsonapi/node/page/' . $unpublished_node->uuid());

    // Unlike the roles test which requests a list, JSON API sends a 403 status
    // code when requesting a specific unauthorized resource instead of list.
    $this->expectException(ClientException::class);
    $this->expectExceptionMessage("Client error: `GET $url` resulted in a `403 Forbidden`");
    $this->container->get('http_client')->get($url);
  }

  /**
   * Makes a request to the API using an optional OAuth token.
   *
   * @param string $endpoint
   *   Path to the API endpoint.
   * @param string $method
   *   The RESTful verb.
   * @param string $token
   *   (optional) A valid OAuth token to send as an Authorization header with
   *   the request.
   * @param array $data
   *   (optional) Additional JSON data to send with the request.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The response from the request.
   */
  private function request($endpoint, $method = 'get', $token = NULL, array $data = NULL) {
    $options = [];
    if ($token) {
      $options = [
        'headers' => [
          'Authorization' => 'Bearer ' . $token,
          'Content-Type' => 'application/vnd.api+json',
        ],
      ];
    }
    if ($data) {
      $options['json'] = $data;
    }

    $url = $this->buildUrl($endpoint);
    return $this->getHttpClient()->$method($url, $options);
  }

  /**
   * Decodes a JSON response from the server.
   *
   * @param \Psr\Http\Message\ResponseInterface $response
   *   The response object.
   *
   * @return mixed
   *   The decoded response data. If the JSON parser raises an error, the test
   *   will fail, with the bad input as the failure message.
   */
  private function decodeResponse(ResponseInterface $response) {
    $body = (string) $response->getBody();

    $data = Json::decode($body);
    if (json_last_error() === JSON_ERROR_NONE) {
      return $data;
    }
    else {
      $this->fail($body);
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc