Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions public/modules/custom/asu_rest/asu_rest.install
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,13 @@ function asu_rest_provision_oauth_consumer(): void {
'client_id' => $client_id,
'label' => $label,
'description' => 'Machine client for Elasticsearch-compatible REST endpoints (projects, apartments).',
'grant_types' => ['client_credentials'],
'scopes' => ['rest_client'],
// simple_oauth implements OAuth2 scopes as Drupal user roles on the
// consumer entity.
'roles' => ['rest_client'],
'user_id' => (int) $user->id(),
'confidential' => TRUE,
'secret' => $secret,
'redirect' => ['https://127.0.0.1/oauth2-callback-not-used'],
'redirect' => 'https://127.0.0.1/oauth2-callback-not-used',
'third_party' => FALSE,
'is_default' => FALSE,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@
*/
class ElasticSearch extends ResourceBase {

/**
* Convert a decimal euro string to integer cents.
*
* Drupal stores monetary values in decimal fields (scale 2). Consumers of
* this endpoint expect values in cents to match the other services.
*/
protected function toCents(?string $value): int {
if ($value === NULL || $value === '') {
return 0;
}
return (int) round(((float) $value) * 100);
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -331,10 +344,10 @@ public function post(array $data): ModifiedResourceResponse | ResourceResponse {
'apartment_state_of_sale' => $apartment_state_of_sale,
'apartment_structure' => $apartment_structure,
'application_url' => $application_url,
'debt_free_sales_price' => floatval($get_scalar($apartment_node, 'field_debt_free_sales_price') ?: 0),
'debt_free_sales_price' => $this->toCents($get_scalar($apartment_node, 'field_debt_free_sales_price')),
'floor' => $get_scalar($apartment_node, 'field_floor') !== '' ? intval($get_scalar($apartment_node, 'field_floor')) : NULL,
'floor_max' => $get_scalar($apartment_node, 'field_floor_max') !== '' ? intval($get_scalar($apartment_node, 'field_floor_max')) : NULL,
'housing_company_fee' => floatval($get_scalar($apartment_node, 'field_housing_company_fee') ?: 0),
'housing_company_fee' => $this->toCents($get_scalar($apartment_node, 'field_housing_company_fee')),
'living_area' => floatval($get_scalar($apartment_node, 'field_living_area') ?: 0),
'nid' => intval($apartment_node->id()),
'project_application_end_time' => $project_application_end_time,
Expand All @@ -357,12 +370,12 @@ public function post(array $data): ModifiedResourceResponse | ResourceResponse {
'project_upcoming_description' => $get_scalar($project_node, 'field_upcoming_description'),
'project_url' => $this->buildAbsoluteUrl($project_node->toUrl()->toString()),
'project_uuid' => $project_node->uuid(),
'release_payment' => floatval($get_scalar($apartment_node, 'field_release_payment') ?: 0),
'right_of_occupancy_payment' => floatval($get_scalar($apartment_node, 'field_right_of_occupancy_payment') ?: 0),
'release_payment' => $this->toCents($get_scalar($apartment_node, 'field_release_payment')),
'right_of_occupancy_payment' => $this->toCents($get_scalar($apartment_node, 'field_right_of_occupancy_payment')),
'title' => $apartment_node->label(),
'url' => $this->buildAbsoluteUrl($apartment_node->toUrl()->toString()),
'uuid' => $apartment_node->uuid(),
'sales_price' => floatval($get_scalar($apartment_node, 'field_sales_price') ?: 0),
'sales_price' => $this->toCents($get_scalar($apartment_node, 'field_sales_price')),
'room_count' => $room_count,
// For accurate FE structure, add more fields as needed.
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,32 @@ protected function setUp(): void {
$this->installEntitySchema('consumer');

// Needed by consumers/simple_oauth entities.
$this->installConfig(['system', 'user']);
$this->installConfig([
'system',
'user',
'consumers',
'simple_oauth',
]);

// Role expected to exist via CMI in real environments.
Role::create([
'id' => 'rest_client',
'label' => 'REST api client',
])->save();

// asu_rest provisioning requires the rest_client scope when the
// oauth2_scope entity type exists in the environment.
$entity_type_manager = $this->container->get('entity_type.manager');
if ($entity_type_manager->hasDefinition('oauth2_scope')) {
$scope_storage = $entity_type_manager->getStorage('oauth2_scope');
if (!$scope_storage->load('rest_client')) {
$scope_storage->create([
'id' => 'rest_client',
'label' => 'REST client',
'description' => 'REST client scope for automated consumers.',
])->save();
}
}
}

/**
Expand All @@ -65,6 +84,8 @@ public function tearDown(): void {
*/
public function testProvisioningCreatesUserAndAssignsConsumer(): void {
putenv('ASU_REST_OAUTH_CLIENT_SECRET=test-secret');
// Ensure container environment does not override configured client_id.
putenv('ASU_REST_OAUTH_CLIENT_ID=apartment_application_service');
putenv('ASU_REST_OAUTH_DEFAULT_USERNAME=rest_client');

$this->config('asu_rest.settings')
Expand All @@ -87,11 +108,17 @@ public function testProvisioningCreatesUserAndAssignsConsumer(): void {
$this->assertTrue($user->isActive());
$this->assertTrue(in_array('rest_client', $user->getRoles(), TRUE));

$consumers = $this->container->get('entity_type.manager')
->getStorage('consumer')
->loadByProperties(['client_id' => 'apartment_application_service']);
$consumer_storage = $this->container->get('entity_type.manager')
->getStorage('consumer');
$consumer_ids = $consumer_storage->getQuery()
->accessCheck(FALSE)
->condition('client_id', 'apartment_application_service')
->range(0, 1)
->execute();
$this->assertNotEmpty($consumer_ids);

/** @var \Drupal\consumers\Entity\Consumer $consumer */
$consumer = reset($consumers);
$consumer = $consumer_storage->load(reset($consumer_ids));
$this->assertInstanceOf(Consumer::class, $consumer);
$this->assertSame((int) $user->id(), (int) $consumer->get('user_id')->target_id);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\asu_rest\Unit;

use Drupal\asu_rest\Plugin\rest\resource\ElasticSearch;
use Drupal\Tests\UnitTestCase;

/**
* Tests cents conversion used by the /elasticsearch resource.
*
* @group asu_rest
*/
final class ElasticSearchToCentsTest extends UnitTestCase {

/**
* Tests that euro decimals are converted to integer cents.
*
* @dataProvider centsProvider
*/
public function testToCents(?string $input, int $expected): void {
$ref = new \ReflectionClass(ElasticSearch::class);
/** @var \Drupal\asu_rest\Plugin\rest\resource\ElasticSearch $resource */
$resource = $ref->newInstanceWithoutConstructor();

Check warning on line 25 in public/modules/custom/asu_rest/tests/src/Unit/ElasticSearchToCentsTest.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure that this accessibility bypass is safe here.

See more on https://sonarcloud.io/project/issues?id=City-of-Helsinki_drupal-asuntotuotanto&issues=AZ2v9lqIJlNvP6keb1gi&open=AZ2v9lqIJlNvP6keb1gi&pullRequest=871

$method = new \ReflectionMethod(ElasticSearch::class, 'toCents');
$method->setAccessible(TRUE);

Check warning on line 28 in public/modules/custom/asu_rest/tests/src/Unit/ElasticSearchToCentsTest.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure that this accessibility update is safe here.

See more on https://sonarcloud.io/project/issues?id=City-of-Helsinki_drupal-asuntotuotanto&issues=AZ2v9lqIJlNvP6keb1gj&open=AZ2v9lqIJlNvP6keb1gj&pullRequest=871

$actual = $method->invoke($resource, $input);
$this->assertSame($expected, $actual);
}

/**
* Data provider for cents conversion.
*
* @return array<string, array{0: ?string, 1: int}>
* Test cases keyed by human-readable description.
*/
public static function centsProvider(): array {
return [
'null becomes zero' => [NULL, 0],
'empty becomes zero' => ['', 0],
'zero becomes zero' => ['0', 0],
'integer euros' => ['123', 12300],
'two decimals' => ['123.45', 12345],
'one decimal rounds' => ['123.4', 12340],
'round half up-ish' => ['0.005', 1],
];
}

}
Loading