xero_sync-8.x-1.x-dev/modules/xero_sync_user_contact/tests/src/Kernel/QueuingProcessingTest.php
modules/xero_sync_user_contact/tests/src/Kernel/QueuingProcessingTest.php
<?php
namespace Drupal\Tests\xero_sync_user_contact\Kernel;
use Drupal\xero\XeroNullClient;
use Drupal\xero_sync\Plugin\QueueWorker\WorkerBase;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Radcliffe\Xero\XeroClient;
use GuzzleHttp\Psr7\Response;
/**
* Test processing queue items.
*
* @group xero_sync
* @group legacy
*/
class QueuingProcessingTest extends XeroSyncUserContactTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'serialization',
'xero',
'xero_sync',
'xero_sync_user_contact',
];
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $logger;
/**
* The Xero client.
*
* @var \Radcliffe\Xero\XeroClient|\PHPUnit\Framework\MockObject\MockObject
*/
protected $xeroClient;
/**
* The FQCN of the class responsible for processing queue jobs.
*
* @var string
*/
protected $jobClass = WorkerBase::class;
/**
* {@inheritDoc}
*/
protected function setUp(): void {
/*
* The QueuingProcessingTest and AdvancedQueuingProcessingTest
* should be refactored.
* It would be better to use ItemManager instead of Xero client for mocking.
*/
$this->markTestSkipped('The tests should be refactored.');
parent::setUp();
$this->disableEntityHandler();
$this->logger = $this->createMock(LoggerInterface::class);
$this->xeroClient = $this->getMockBuilder(XeroClient::class)
->disableOriginalConstructor()
->getMock();
$this->container->set('logger.channel.xero_sync', $this->logger);
$this->container->set('xero.client', $this->xeroClient);
$jobClass = new \ReflectionClass($this->jobClass);
$waitProperty = $jobClass->getProperty('waitUntil');
$waitProperty->setAccessible(TRUE);
$waitProperty->setValue(0);
}
/**
* Test what happens if a job is queued for a user who doesn't exist.
*
* Might happen if a user was deleted.
*/
public function testUserDoesntExist() {
// No queries, we just log and return quickly.
$this->logger->expects($this->once())
->method('log')
->with(
RfcLogLevel::ERROR,
$this->stringContains('No valid entity')
);
$this->xeroClient->expects($this->never())
->method('__call');
$this->enqueueJob();
$this->runQueueJob();
}
/**
* Test the creation of a new item if no corresponding item exists.
*/
public function testContactIsCreatedIfDidntExist() {
$this->mockLogSuccess();
$response = $this->getMockResponseForEmpty();
$this->mockFindByEmail($response);
$response = $this->getMockResponseFor("{$this->username} ({$this->uid})", $this->email, $this->uid);
$this->xeroClient->expects($this->at(1))
->method('__call')
->with('put')
->willReturn($response);
$this->assertUserCreation(TRUE);
}
/**
* Test if an existing item is found that corresponds to a new user.
*
* The item should be referenced from the user, but not updated if there is no
* new information on it that needs updating.
*/
public function testContactIsReferencedIfFoundByEmail() {
$this->mockLogSuccess();
// Contact number is already set so there will be no need to update that.
$response = $this->getMockResponseFor("{$this->username} ({$this->uid})", $this->email, $this->uid);
$this->mockFindByEmail($response);
$this->assertUserCreation(TRUE);
}
/**
* Test if an existing item is found that corresponds to a new user.
*
* The item should be updated and referenced from the user.
*/
public function testContactIsUpdatedIfFoundByEmail() {
$this->mockLogSuccess();
// Contact number is not set so updating will be needed.
$response = $this->getMockResponseFor("{$this->username} ({$this->uid})", $this->email);
$this->mockFindByEmail($response);
$postResponse = $this->getMockResponseFor("{$this->username} ({$this->uid})", $this->email, $this->uid);
$this->xeroClient->expects($this->at(1))
->method('__call')
->with('post')
->willReturn($postResponse);
$this->assertUserCreation(TRUE);
}
/**
* Test an empty response when creating.
*/
public function testEmptyResponseWhenCreating() {
$this->mockLogException();
$response = $this->getMockResponseForEmpty();
$this->mockFindByEmail($response);
// An empty response to creation.
$this->xeroClient->expects($this->at(1))
->method('__call')
->with('put')
->willReturn($response);
$this->assertUserCreation(FALSE);
}
/**
* Test a bad response when finding.
*/
public function testBadResponseWhenFinding() {
$this->mockLogException();
$this->mockFindByEmail($this->getErrorResponse(500));
$this->assertUserCreation(FALSE);
}
/**
* Test a bad response when creating.
*/
public function testBadResponseWhenCreating() {
$this->mockLogException();
$response = $this->getMockResponseForEmpty();
$this->mockFindByEmail($response);
$this->xeroClient->expects($this->at(1))
->method('__call')
->with('put')
->willReturn($this->getErrorResponse(500));
$this->assertUserCreation(FALSE);
}
/**
* Test an API error when finding.
*/
public function testApiErrorWhenFinding() {
$this->mockLogException();
$this->xeroClient->expects($this->at(0))
->method('__call')
->with('get')
->willThrowException($this->getRequestException(500));
$this->assertUserCreation(FALSE);
}
/**
* Test throwing an exception when creating.
*/
public function testApiErrorWhenCreating() {
$this->mockLogException();
$response = $this->getMockResponseForEmpty();
$this->mockFindByEmail($response);
$this->xeroClient->expects($this->at(1))
->method('__call')
->with('put')
->willThrowException($this->getRequestException(500));
$this->assertUserCreation(FALSE);
}
/**
* Test an API error when finding.
*/
public function testInvalidClientWhenFinding() {
$this->logger->expects($this->once())
->method('log')
->with(
RfcLogLevel::ERROR,
$this->stringContains('Xero client unavailable when trying to process job')
);
$this->xeroClient = $this->createMock(XeroNullClient::class);
$this->container->set('xero.client', $this->xeroClient);
$this->assertUserCreation(FALSE);
}
/**
* Test the API rate limit being exceeded when finding.
*/
public function testMinuteRateLimitExceededWhenFinding() {
$this->logger->expects($this->at(0))
->method('log')
->with(
RfcLogLevel::WARNING,
$this->stringContains('Xero rate limit exceeded while processing xero_sync_sync queue, retry in 5 seconds')
);
$this->logger->expects($this->at(1))
->method('log')
->with(
RfcLogLevel::INFO,
$this->stringContains('Success in queue')
);
$this->xeroClient->expects($this->at(0))
->method('__call')
->with('get')
->willThrowException($this->getRequestException(429, '5'));
// The job should be retried after a 5 second delay.
$response = $this->getMockResponseFor("{$this->username} ({$this->uid})", $this->email, $this->uid);
$this->xeroClient->expects($this->at(1))
->method('__call')
->with('get')
->willReturn($response);
$this->expectEntityUpdates(1);
$this->user->save();
$start = time();
$this->enqueueJob();
$this->runQueueJob();
$this->runQueueJob();
$now = time();
// $this->assertTrue( $now > $start + 4, "The job retry should have paused for 5 seconds. Start:$start Now:$now");
// $this->assertItemReferencedInField($this->user, 'xero_contact', $this->xeroId);
}
/**
* Test the API rate limit being exceeded when finding.
*/
public function testHourRateLimitExceededWhenFinding() {
$this->logger->expects($this->once())
->method('log')
->with(
RfcLogLevel::WARNING,
$this->stringContains('Xero rate limit exceeded while processing xero_sync_sync queue, retry in 65 seconds')
);
$this->xeroClient->expects($this->once())
->method('__call')
->with('get')
->willThrowException($this->getRequestException(429, '65'));
// The job will not be retried. We should test this better.
$this->assertUserCreation(FALSE);
$this->assertNoItemReferencedInField($this->user);
}
/**
* Mock finding an item by email.
*
* @param string $response
* The response to expect. Not sure what data type should be.
*/
protected function mockFindByEmail($response) {
$this->xeroClient->expects($this->at(0))
->method('__call')
->with('get')
->willReturn($response);
}
/**
* Expect success to be logged.
*/
protected function mockLogSuccess() {
$this->logger->expects($this->once())
->method('log')
->with(
RfcLogLevel::INFO,
$this->stringContains('Success in queue')
);
}
/**
* Expect an exception to be logged.
*/
protected function mockLogException($count = 1) {
$this->logger->expects($this->exactly($count))
->method('log')
->with(
RfcLogLevel::ERROR,
$this->stringContains('Exception in queue')
);
}
/**
* Get a mock request exception.
*
* @param int $code
* The response status code.
* @param string $retryAfter
* (optional) How many seconds to retry after, a response header.
*
* @return \GuzzleHttp\Exception\RequestException
* A request exception with mock request and response.
*/
protected function getRequestException($code, $retryAfter = NULL) {
$mockRequest = $this->createMock(RequestInterface::class);
$mockRequest->expects($this->any())
->method('getBody')
->willReturn($this->getBody());
$mockResponse = $this->getErrorResponse($code);
if ($retryAfter) {
$mockResponse->expects($this->any())
->method('hasHeader')
->with('Retry-After')
->willReturn(TRUE);
$mockResponse->expects($this->any())
->method('getHeader')
->with('Retry-After')
->willReturn([$retryAfter]);
}
$e = new RequestException('Xero API error', $mockRequest, $mockResponse);
return $e;
}
/**
* Get a mock response for an error.
*
* @param int $code
* An http status code.
*
* @return \PHPUnit\Framework\MockObject\MockObject
* A mock response.
*/
protected function getErrorResponse($code) {
$mockResponse = $this->createMock(ResponseInterface::class);
$mockResponse->expects($this->any())
->method('getStatusCode')
->willReturn($code);
$mockResponse->expects($this->any())
->method('getBody')
->willReturn($this->getBody());
return $mockResponse;
}
/**
* Get the find by email options.
*/
protected function getFindByEmailOptions() {
NULL;
// @todo re-enable this once https://www.drupal.org/project/xero/issues/3156893
// is committed and the Xero headers stabilise.
// ['Contacts', ['query' => ['where' => 'EmailAddress=="' . $this->email . '"']]];
}
/**
* Get an unexpected response object.
*
* @return \PHPUnit\Framework\MockObject\MockObject
* A mock response object.
*/
protected function getBody() {
$body = $this->createMock(StreamInterface::class);
$body->expects($this->any())
->method('getContents')
->willReturn("Maybe the Xero API is behaving strangely today");
return $body;
}
/**
* Get a block of Xml representing an empty Xero response.
*/
protected function getMockResponseForEmpty() {
$xml = '<Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Id>1040dd4d-e6bd-4430-9844-6345fb214f38</Id>
<Status>OK</Status>
<ProviderName>Drupal Xero</ProviderName>
<DateTimeUTC>2018-09-27T03:18:43.3794355Z</DateTimeUTC>
</Response>';
$response = new Response(
200,
[
'Content-Type' => 'text/xml',
],
$xml
);
return $response;
}
/**
* Get a block of xml representing a successful Xero response.
*/
protected function getMockResponseFor($name = '', $mail = '', $contactNumber = '') {
$xml = '<Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Id>1040dd4d-e6bd-4430-9844-6345fb214f38</Id>
<Status>OK</Status>
<ProviderName>Drupal Xero</ProviderName>
<DateTimeUTC>2018-09-27T03:18:43.3794355Z</DateTimeUTC>
<Contacts>
<Contact>
<ContactID>' . $this->xeroId . '</ContactID>
<ContactNumber>' . $contactNumber . '</ContactNumber>
<ContactStatus>ACTIVE</ContactStatus>
<Name>' . $name . '</Name>
<FirstName></FirstName>
<LastName></LastName>
<EmailAddress>' . $mail . '</EmailAddress>
<Addresses>
<Address>
<AddressType>POBOX</AddressType>
</Address>
<Address>
<AddressType>STREET</AddressType>
</Address>
</Addresses>
<Phones>
<Phone>
<PhoneType>DDI</PhoneType>
</Phone>
<Phone>
<PhoneType>DEFAULT</PhoneType>
</Phone>
<Phone>
<PhoneType>FAX</PhoneType>
</Phone>
<Phone>
<PhoneType>MOBILE</PhoneType>
</Phone>
</Phones>
<UpdatedDateUTC>2018-09-20T14:51:53.56</UpdatedDateUTC>
<IsSupplier>false</IsSupplier>
<IsCustomer>false</IsCustomer>
<HasAttachments>false</HasAttachments>
</Contact>
</Contacts>
</Response>';
$response = new Response(
200,
[
'Content-Type' => 'text/xml',
],
$xml
);
return $response;
}
/**
* Create a user, run the queue, and perhaps assert that an item is stored.
*
* @param bool $referenced
* Whether or not to expect an item to be referenced on the user.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function assertUserCreation($referenced) {
$this->expectEntityUpdates((int) $referenced);
$this->user->save();
$this->enqueueJob();
$this->runQueueJob();
if ($referenced) {
$this->assertItemReferencedInField($this->user, 'xero_contact', $this->xeroId);
}
else {
$this->assertNoItemReferencedInField($this->user);
}
}
/**
* Create the queue job.
*/
protected function enqueueJob() {}
/**
* Process the queue job.
*
* @var int $repeat
* How many times to process.
*/
protected function runQueueJob($repeat = 1) {
$queue_name = 'xero_sync_sync';
/** @var \Drupal\Core\Queue\QueueWorkerInterface $queue_worker */
$queue_worker = \Drupal::service('plugin.manager.queue_worker')
->createInstance($queue_name);
for ($k = 0; $k < $repeat; $k++) {
$queue_worker->processItem(
[
'entity_id' => $this->uid,
'entity_type' => 'user',
]);
}
}
}
