Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • kompetenztest.de/tba2/testcenter-connector
1 result
Show changes
Commits on Source (7)
Showing
with 266 additions and 92 deletions
......@@ -4,6 +4,7 @@ require __DIR__ . '/../vendor/autoload.php';
use GuzzleHttp\Client;
use Kompetenztestde\TestcenterConnector\Api\TestcenterApiClient;
use Kompetenztestde\TestcenterConnector\ErrorLogger;
use Kompetenztestde\TestcenterConnector\Models\Pupil;
use Kompetenztestde\TestcenterConnector\Models\Teacher;
use Kompetenztestde\TestcenterConnector\TestcenterFacadeImpl;
......@@ -11,11 +12,12 @@ use Kompetenztestde\TestcenterConnector\Xml\XmlGenerator;
use Kompetenztestde\TestcenterConnector\Models\TestGroup;
$httpClient = new Client();
$errorLogger = new ErrorLogger();
$apiClient = new TestcenterApiClient('http://localhost/api', $httpClient);
$xmlGenerator = new XmlGenerator();
$connector = new TestcenterFacadeImpl($apiClient, $xmlGenerator, 'super', 'user123');
$connector = new TestcenterFacadeImpl($apiClient, $xmlGenerator, $errorLogger, 'super', 'user123');
$logins = [
new Pupil('login1', 'password1'),
......
......@@ -2,14 +2,13 @@
namespace Kompetenztestde\TestcenterConnector\Api;
use Exception;
use GuzzleHttp;
use Kompetenztestde\TestcenterConnector\Api\Schemas\StartSessionResponse;
use Kompetenztestde\TestcenterConnector\Api\Schemas\FileUploadResponse;
use Kompetenztestde\TestcenterConnector\Api\Schemas\FileValidationResult;
use Kompetenztestde\TestcenterConnector\Exceptions;
use Kompetenztestde\TestcenterConnector\Exceptions\AuthorizationException;
use Kompetenztestde\TestcenterConnector\Exceptions\TestcenterServerException;
use Kompetenztestde\TestcenterConnector\Exceptions\InvalidResponseException;
class TestcenterApiClient
{
......@@ -52,7 +51,7 @@ class TestcenterApiClient
]);
} catch (GuzzleHttp\Exception\ClientException $e) {
if ($e->getResponse()->getStatusCode() === 400) {
throw new Exceptions\AuthenticationException('Invalid credentials');
throw new Exceptions\AuthenticationException('Invalid credentials', 0, $e);
}
$this->handleException($e);
} catch (GuzzleHttp\Exception\TransferException $e) {
......@@ -67,7 +66,7 @@ class TestcenterApiClient
$data = json_decode($body);
if (!isset($data->token)) {
throw new TestcenterServerException('Field "token" is missing in response');
throw new InvalidResponseException('Field "token" is missing in response', $response);
}
$sessionResponse = new StartSessionResponse($data->token);
......@@ -101,7 +100,7 @@ class TestcenterApiClient
$response = $this->httpClient->post($this->baseUrl . '/workspace/' . $workspaceId . '/file', $requestOptions);
} catch (GuzzleHttp\Exception\ClientException $e) {
if ($e->getResponse()->getStatusCode() === 400) {
throw new Exceptions\InvalidRequestException('Invalid file');
throw new Exceptions\InvalidRequestException('Invalid file', 0, $e);
}
$this->handleException($e);
} catch (GuzzleHttp\Exception\TransferException $e) {
......@@ -136,35 +135,35 @@ class TestcenterApiClient
/**
* Handle guzzle exceptions that aren't special for a specific request
*/
private function handleException(Exception $e): void
private function handleException(GuzzleHttp\Exception\TransferException $e): void
{
if ($e instanceof GuzzleHttp\Exception\ClientException) {
switch ($e->getResponse()->getStatusCode()) {
case 401:
throw new Exceptions\AuthenticationException();
throw Exceptions\AuthenticationException::createWithPrevious($e);
case 403:
throw new Exceptions\AuthorizationException();
throw Exceptions\AuthorizationException::createWithPrevious($e);
case 404:
throw new Exceptions\NotFoundException();
throw Exceptions\NotFoundException::createWithPrevious($e);
case 409:
throw new Exceptions\ConflictException();
throw Exceptions\ConflictException::createWithPrevious($e);
case 410:
throw new Exceptions\AuthenticationException('Session expired');
throw new Exceptions\AuthenticationException('Session expired', 0, $e);
case 413:
throw new Exceptions\InvalidRequestException('File too large');
throw new Exceptions\InvalidRequestException('File too large', 0, $e);
default:
throw new Exceptions\InvalidRequestException($e->getMessage());
throw Exceptions\InvalidRequestException::createWithPrevious($e);
}
} else if ($e instanceof GuzzleHttp\Exception\ServerException) {
throw new Exceptions\TestcenterServerException($e->getMessage());
throw Exceptions\InternalServerErrorException::createWithPrevious($e);
} else if ($e instanceof GuzzleHttp\Exception\ConnectException) {
throw new Exceptions\TestcenterConnectException($e->getMessage());
throw Exceptions\ConnectionException::createWithPrevious($e);
}
throw $e;
......
<?php
namespace Kompetenztestde\TestcenterConnector;
use Exception;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Kompetenztestde\TestcenterConnector\Exceptions\InvalidResponseException;
class ErrorLogger implements ErrorLoggerInterface
{
public function logException(Exception $e): void
{
if ($e instanceof InvalidResponseException) {
$message = $e->getMessage() . ' (Response: ' . $this->formatResponseMessage($e->getResponse()) . ')';
} else {
$message = $e->getMessage();
if ($e->getPrevious() && $e->getPrevious() instanceof TransferException) {
$message .= ' (Reason: ' . $this->formatGuzzleException($e->getPrevious()) . ')';
}
}
error_log($message);
}
private function formatGuzzleException(TransferException $e): string
{
if ($e instanceof ConnectException) {
return $e->getMessage();
} else if ($e instanceof BadResponseException) {
return 'Request: ' . $this->formatRequestMessage($e->getRequest())
. '; Response: ' . $this->formatResponseMessage($e->getResponse());
}
}
private function formatRequestMessage(Request $request): string
{
$message = $request->getMethod() . ' ' . $request->getRequestTarget();
if ($request->getBody()->getSize()) {
$message .= ' ' . $request->getBody()->getContents();
}
return $message;
}
private function formatResponseMessage(Response $response): string
{
return $response->getStatusCode() . ' ' . $response->getBody()->getContents();
}
}
\ No newline at end of file
<?php
namespace Kompetenztestde\TestcenterConnector;
use Exception;
interface ErrorLoggerInterface
{
public function logException(Exception $e): void;
}
\ No newline at end of file
......@@ -2,7 +2,12 @@
namespace Kompetenztestde\TestcenterConnector\Exceptions;
class AuthenticationException extends TestcenterConnectorException
use Throwable;
class AuthenticationException extends TestcenterServerException
{
public static function createWithPrevious(?Throwable $previous): AuthenticationException
{
return new AuthenticationException('Authentication Error', 0, $previous);
}
}
\ No newline at end of file
......@@ -2,10 +2,12 @@
namespace Kompetenztestde\TestcenterConnector\Exceptions;
class AuthorizationException extends TestcenterConnectorException
use Throwable;
class AuthorizationException extends TestcenterServerException
{
public function __construct()
public static function createWithPrevious(?Throwable $previous): AuthorizationException
{
parent::__construct('You are not authorized to perform this operation.');
return new AuthorizationException('Authorization Error', 0, $previous);
}
}
\ No newline at end of file
......@@ -2,10 +2,12 @@
namespace Kompetenztestde\TestcenterConnector\Exceptions;
class ConflictException extends TestcenterConnectorException
use Throwable;
class ConflictException extends TestcenterServerException
{
public function __construct()
public static function createWithPrevious(?Throwable $previous): ConflictException
{
parent::__construct('Operation failed because of an conflicting access on the same resource.');
return new ConflictException('Conflicting access on the same resource', 0, $previous);
}
}
\ No newline at end of file
<?php
namespace Kompetenztestde\TestcenterConnector\Exceptions;
use Throwable;
class ConnectionException extends TestcenterConnectorException
{
public static function createWithPrevious(?Throwable $previous): ConnectionException
{
return new ConnectionException('Testcenter is not reachable', 0, $previous);
}
}
\ No newline at end of file
<?php
namespace Kompetenztestde\TestcenterConnector\Exceptions;
use Throwable;
class InternalServerErrorException extends TestcenterServerException
{
public static function createWithPrevious(?Throwable $previous): InternalServerErrorException
{
return new InternalServerErrorException('Internal server error', 0, $previous);
}
}
\ No newline at end of file
......@@ -2,7 +2,12 @@
namespace Kompetenztestde\TestcenterConnector\Exceptions;
class InvalidRequestException extends TestcenterConnectorException
use Throwable;
class InvalidRequestException extends TestcenterServerException
{
public static function createWithPrevious(Throwable $previous): InternalServerErrorException
{
return new InternalServerErrorException('Invalid Request', 0, $previous);
}
}
\ No newline at end of file
<?php
namespace Kompetenztestde\TestcenterConnector\Exceptions;
use GuzzleHttp\Psr7\Response;
class InvalidResponseException extends TestcenterServerException
{
/**
* @var Response
*/
private $response;
/**
* @param string $message
* @param Response $response
*/
public function __construct(string $message, Response $response)
{
$this->response = $response;
parent::__construct($message);
}
/**
* @return Response
*/
public function getResponse(): Response
{
return $this->response;
}
}
\ No newline at end of file
......@@ -2,10 +2,12 @@
namespace Kompetenztestde\TestcenterConnector\Exceptions;
use Throwable;
class NotFoundException extends TestcenterConnectorException
{
public function __construct()
public static function createWithPrevious(?Throwable $previous): NotFoundException
{
parent::__construct('The resource was not found.');
return new NotFoundException('The resource was not found', 0, $previous);
}
}
\ No newline at end of file
<?php
namespace Kompetenztestde\TestcenterConnector\Exceptions;
class TestcenterConnectException extends TestcenterConnectorException
{
/**
* @param string|null $reason
*/
public function __construct(?string $reason = null)
{
$message = 'Testcenter is not reachable';
if ($reason) {
$message .= ' (Reason: ' . $reason . ')';
}
parent::__construct($message);
}
}
\ No newline at end of file
......@@ -6,5 +6,4 @@ use Exception;
class TestcenterConnectorException extends Exception
{
}
\ No newline at end of file
......@@ -4,17 +4,4 @@ namespace Kompetenztestde\TestcenterConnector\Exceptions;
class TestcenterServerException extends TestcenterConnectorException
{
/**
* @param string|null $reason
*/
public function __construct(?string $reason = null)
{
$message = 'Testcenter Server Error';
if ($reason) {
$message .= ' (Reason: ' . $reason . ')';
}
parent::__construct($message);
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ namespace Kompetenztestde\TestcenterConnector;
use Kompetenztestde\TestcenterConnector\Xml;
use Kompetenztestde\TestcenterConnector\Api\TestcenterApiClient;
use Kompetenztestde\TestcenterConnector\Exceptions\TestcenterConnectorException;
use Kompetenztestde\TestcenterConnector\Models;
class TestcenterFacadeImpl implements TestcenterFacadeInterface
......@@ -28,36 +29,49 @@ class TestcenterFacadeImpl implements TestcenterFacadeInterface
*/
private $adminPassword;
/**
* @var ErrorLoggerInterface
*/
private $errorLogger;
/**
* Create a TestcenterFacadeImpl instance
*
* @param TestcenterApiClient $apiClient
* @param XMLGenerator $xmlGenerator
* @param ErrorLoggerInterface $errorLogger
* @param string $adminUsername
* @param string $adminPassword
*/
public function __construct(TestcenterApiClient $apiClient,
Xml\XMLGenerator $xmlGenerator,
ErrorLoggerInterface $errorLogger,
string $adminUsername,
string $adminPassword)
{
$this->xmlGenerator = $xmlGenerator;
$this->apiClient = $apiClient;
$this->errorLogger = $errorLogger;
$this->adminUsername = $adminUsername;
$this->adminPassword = $adminPassword;
}
public function addTestGroup(int $workspaceId, Models\TestGroup $group, string $bookletId)
{
// generate testtakers xml
$xmlGroup = $this->getXmlTestGroup($group, $bookletId);
$testtakers = $this->xmlGenerator->generateTesttakersXml($xmlGroup);
try {
// generate testtakers xml
$xmlGroup = $this->getXmlTestGroup($group, $bookletId);
$testtakers = $this->xmlGenerator->generateTesttakersXml($xmlGroup);
// upload testtakers xml
$authToken = $this->getAdminAuthToken();
// upload testtakers xml
$authToken = $this->getAdminAuthToken();
$filename = 'testtakers-' . $group->getId() . '.xml';
$this->apiClient->uploadFile($authToken, $workspaceId, $filename, $testtakers);
$filename = 'testtakers-' . $group->getId() . '.xml';
$this->apiClient->uploadFile($authToken, $workspaceId, $filename, $testtakers);
} catch (TestcenterConnectorException $e) {
$this->errorLogger->logException($e);
throw $e;
}
}
private function getXmlTestGroup(Models\TestGroup $group, string $bookletId): Xml\TestGroup
......
......@@ -11,9 +11,10 @@ use GuzzleHttp\Psr7\Response;
use Kompetenztestde\TestcenterConnector\Exceptions\AuthenticationException;
use Kompetenztestde\TestcenterConnector\Exceptions\AuthorizationException;
use Kompetenztestde\TestcenterConnector\Exceptions\ConflictException;
use Kompetenztestde\TestcenterConnector\Exceptions\ConnectionException;
use Kompetenztestde\TestcenterConnector\Exceptions\InternalServerErrorException;
use Kompetenztestde\TestcenterConnector\Exceptions\InvalidRequestException;
use Kompetenztestde\TestcenterConnector\Exceptions\NotFoundException;
use Kompetenztestde\TestcenterConnector\Exceptions\TestcenterConnectException;
use Kompetenztestde\TestcenterConnector\Exceptions\TestcenterServerException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
......@@ -68,7 +69,6 @@ class TestcenterApiClientTest extends TestCase
public function testStartAdminSessionClientError(int $statusCode, string $exception): void
{
$this->mock->append(new Response($statusCode, [], ''));
$this->expectException($exception);
$this->apiClient->startAdminSession('super', 'user123');
}
......@@ -84,7 +84,7 @@ class TestcenterApiClientTest extends TestCase
public function testStartAdminSessionServerError(): void
{
$this->mock->append(new Response(500, [], ''));
$this->expectException(TestcenterServerException::class);
$this->expectException(InternalServerErrorException::class);
$this->apiClient->startAdminSession('super', 'user123');
}
......@@ -98,8 +98,7 @@ class TestcenterApiClientTest extends TestCase
public function testStartAdminSessionConnectionError(): void
{
$this->mock->append(new ConnectException('Failed to connect to server', new Request('POST', '/test')));
$this->expectException(TestcenterConnectException::class);
$this->expectException(ConnectionException::class);
$this->apiClient->startAdminSession('super', 'user123');
}
......@@ -153,7 +152,6 @@ class TestcenterApiClientTest extends TestCase
public function testUploadFileClientError(int $statusCode, string $exception): void
{
$this->mock->append(new Response($statusCode, [], ''));
$this->expectException($exception);
$this->apiClient->uploadFile('test-token', 1, 'SAMPLE_UNIT.XML', 'test');
}
......@@ -161,16 +159,14 @@ class TestcenterApiClientTest extends TestCase
public function testUploadFileServerError(): void
{
$this->mock->append(new Response(500, [], ''));
$this->expectException(TestcenterServerException::class);
$this->expectException(InternalServerErrorException::class);
$this->apiClient->uploadFile('test-token', 1, 'SAMPLE_UNIT.XML', 'test');
}
public function testUploadFileConnectionError(): void
{
$this->mock->append(new ConnectException('Failed to connect to server', new Request('POST', '/test')));
$this->expectException(TestcenterConnectException::class);
$this->expectException(ConnectionException::class);
$this->apiClient->uploadFile('test-token', 1, 'SAMPLE_UNIT.XML', 'test');
}
......
<?php
namespace Kompetenztestde\TestcenterConnector;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Kompetenztestde\TestcenterConnector\Exceptions\InvalidRequestException;
use PHPUnit\Framework\TestCase;
class ErrorLoggerTest extends TestCase
{
/**
* @var ErrorLogger
*/
private $errorLogger;
/**
* @var resource
*/
private $errorLogTmpFile;
/**
* @var string
*/
private $errorLogLocationBackup;
protected function setUp(): void
{
// capture error_log output
$this->errorLogTmpFile = tmpfile();
$streamUri = stream_get_meta_data($this->errorLogTmpFile)['uri'];
$this->errorLogLocationBackup = ini_set('error_log', $streamUri);
$this->errorLogger = new ErrorLogger();
}
public function testLogException(): void
{
$guzzleException = new ClientException('Test error', new Request('GET', '/test'), new Response(400, [], 'Invalid input data'));
$exception = InvalidRequestException::createWithPrevious($guzzleException);
$this->errorLogger->logException($exception);
$errorContents = $this->getErrorLogContents();
$this->assertStringContainsString('Invalid Request', $errorContents);
$this->assertStringContainsString('Invalid input data', $errorContents);
$this->assertStringContainsString('GET /test', $errorContents);
$this->assertStringContainsString('400', $errorContents);
}
protected function tearDown(): void
{
fclose($this->errorLogTmpFile);
}
private function getErrorLogContents(): string
{
ini_set('error_log', $this->errorLogLocationBackup);
return stream_get_contents($this->errorLogTmpFile);
}
}
\ No newline at end of file
......@@ -6,11 +6,6 @@ use Kompetenztestde\TestcenterConnector\Api\Schemas\FileUploadResponse;
use Kompetenztestde\TestcenterConnector\Api\Schemas\StartSessionResponse;
use PHPUnit\Framework\TestCase;
use Kompetenztestde\TestcenterConnector\Api\TestcenterApiClient;
use Kompetenztestde\TestcenterConnector\Exceptions\AuthenticationException;
use Kompetenztestde\TestcenterConnector\Exceptions\AuthorizationException;
use Kompetenztestde\TestcenterConnector\Exceptions\ConflictException;
use Kompetenztestde\TestcenterConnector\Exceptions\InvalidRequestException;
use Kompetenztestde\TestcenterConnector\Exceptions\NotFoundException;
use Kompetenztestde\TestcenterConnector\Xml\XmlGenerator;
use Kompetenztestde\TestcenterConnector\Models\TestGroup;
use Kompetenztestde\TestcenterConnector\Models\Pupil;
......@@ -25,39 +20,42 @@ class TestcenterFacadeImplTest extends TestCase
private $xmlGenerator;
/**
* @val string
* @var string
*/
private $bookletId;
/**
* @val TestGroup
* @var TestGroup
*/
private $group;
/**
* @val string
* @var string
*/
private $authToken;
/**
* @val int
* @var int
*/
private $workspaceId;
/**
* @val string
* @var string
*/
private $testtakersContent;
/**
* @val Login[]
* @var Login[]
*/
private $logins;
private $errorLogger;
protected function setUp(): void
{
$this->apiClient = $this->createMock(TestcenterApiClient::class);
$this->xmlGenerator = $this->createMock(XmlGenerator::class);
$this->errorLogger = $this->createMock(ErrorLoggerInterface::class);
$this->logins = [
new Pupil('login1', 'password1'),
new Teacher('login2', 'password2')
......@@ -93,7 +91,7 @@ class TestcenterFacadeImplTest extends TestCase
)
->willReturn(new FileUploadResponse([]));
$connector = new TestcenterFacadeImpl($this->apiClient, $this->xmlGenerator, 'super', 'user123');
$connector = new TestcenterFacadeImpl($this->apiClient, $this->xmlGenerator, $this->errorLogger, 'super', 'user123');
$connector->addTestGroup($this->workspaceId, $this->group, $this->bookletId);
}
......@@ -124,7 +122,7 @@ class TestcenterFacadeImplTest extends TestCase
->method('uploadFile')
->willReturn(new FileUploadResponse([]));
$connector = new TestcenterFacadeImpl($this->apiClient, $this->xmlGenerator, 'super', 'user123');
$connector = new TestcenterFacadeImpl($this->apiClient, $this->xmlGenerator, $this->errorLogger, 'super', 'user123');
$connector->addTestGroup($this->workspaceId, $group, 'test-booklet');
}
}
\ No newline at end of file