Commit c9817dec authored by Karamel's avatar Karamel
Browse files

Update http error responses, update currency routes, add test.

parent 60a5f42a
......@@ -13,9 +13,47 @@ class APIResponse extends Response
case APIResult::STATUS_CALL_OK:
return $this->withJson($result->getStructContent());
case APIResult::STATUS_CALL_REJECTED:
return $this->withStatus(400, $result->getContent());
if (is_string($result->getContent())) {
return $this->withStatus(400, $result->getContent());
} else {
return $this->reject($result->getContent());
}
case APIResult::STATUS_CALL_ERROR:
return $this->withStatus(500, $result->getContent());
}
}
/**
* Get a reject response caused by an exception.
* @param \Pasteque\Server\Exception\PastequeException $e
* @param string|null $msg Custom response message to match
* those that were defined before sending details in the body.
* @return Response A configured response, with code 400 and the exception
* in the body.
*/
public function reject($e, $msg = null) {
$content = $e->toStruct();
if ($msg !== null) {
return $this->withStatus(400, $msg)->withJson($content);
} else {
return $this->withStatus(400)->withJson($content);
}
}
/**
* Get a 404 response with the according content.
* @param \Pasteque\Server\Exception\RecordNotFoundException $e
* @param string|null $msg Custom response message to match
* those that were defined before sending details in the body.
* @return Response A configured response, with code 400 and the exception
* in the body.
*/
public function notFound($e, $msg = null) {
$content = $e->toStruct();
if ($msg !== null) {
return $this->withStatus(404, $msg)->withJson($content);
} else {
return $this->withStatus(404)->withJson($content);
}
}
}
<?php
use Pasteque\Server\Exception\InvalidFieldException;
use Pasteque\Server\Exception\RecordNotFoundException;
use Pasteque\Server\Model\Currency;
use Pasteque\Server\System\API\APICaller;
use Pasteque\Server\System\API\APIResult;
......@@ -26,9 +28,23 @@ $app->POST('/api/currency',
if ($curr == null) {
$curr = new Currency();
}
$curr->merge($tab,$ptApp->getDao());
$otherCurr = Currency::load($tab['reference'], $ptApp->getDao());
if ($otherCurr !== null) {
if ($curr->getId() != $otherCurr->getId()) {
$loadKey = null;
if (!empty($tab['id'])) {
$loadKey = ['id' => $tab['id']];
} else {
$loadKey = Currency::getLoadKey($tab);
}
$e = new InvalidFieldException(InvalidFieldException::CSTR_UNIQUE,
Currency::class, 'reference', $loadKey, $tab['reference']);
return $response->reject($e, 'Reference is already taken');
}
}
$curr->merge($tab, $ptApp->getDao());
return $response->withApiResult(APICaller::run($ptApp, 'currency', 'write',
$currency));
$curr));
});
/**
......@@ -45,31 +61,45 @@ $app->PUT('/api/currency/{reference}',
return $response->withStatus(400, 'New record cannot have an Id');
}
$tab['reference'] = $args['reference'];
$curr = Currency::load($tab['reference'], $ptApp->getDao());
$curr = Currency::load($args['reference'], $ptApp->getDao());
if ($curr !== null) {
return $response->withStatus(400, 'Reference is already taken');
$loadKey = ['reference' => $args['reference']];
$e = new InvalidFieldException(InvalidFieldException::CSTR_UNIQUE,
Currency::class, 'reference', $loadKey, $curr->getReference());
return $response->reject($e, 'Reference is already taken');
}
$curr->merge($tab);
$curr = new Currency();
$curr->merge($tab, $ptApp->getDao());
return $response->withApiResult(APICaller::run($ptApp, 'currency', 'write',
$currency));
$curr));
});
/**
* Update an existing currency from it's reference. Returns an error if an id
* is given or if there aren't any currency with this reference.
*/
$app->PATCH('api/currency/{reference}',
$app->PATCH('/api/currency/{reference}',
function($request, $response, $args) {
$ptApp = $this->get('settings')['ptApp'];
$tab = $request->getParsedBody();
if (!empty($tab['id'])) {
return $response->withStatus(400, 'Do not send Id, use reference instead.');
}
$curr = Currency::load($tab['reference'], $ptApp->getDao());
$loadKey = ['reference' => $args['reference']];
$curr = Currency::load($loadKey, $ptApp->getDao());
if ($curr === null) {
return $response->withStatus(404, 'No currency found.');
$e = new RecordNotFoundException(Currency::class, $loadKey);
return $response->notFound($e, 'No currency found.');
}
$curr->merge($tab);
return $response->withApiResult(APICaller::run($ptApp, 'currency', 'write',
$currency));
$curr->merge($tab, $ptApp->getDao());
if ($tab['reference'] !== $args['reference']) {
$otherCurr = Currency::structLoad($tab, $ptApp->getDao());
if ($otherCurr !== null) {
$e = new InvalidFieldException(InvalidFieldException::CSTR_UNIQUE,
Currency::class, 'reference', $loadKey,
$curr->getReference());
return $response->reject($e, 'Reference is already taken');
}
}
return $response->withApiResult(APICaller::run($ptApp, 'currency', 'write', $curr));
});
......@@ -22,6 +22,7 @@
namespace Pasteque\Server\API;
use \Pasteque\Server\Exception\InvalidFieldException;
use \Pasteque\Server\System\DAO\DAOCondition;
/** CRUD API for Currency */
......@@ -58,7 +59,9 @@ class CurrencyAPI extends APIRefHelper
}
}
if ($removeMain && !$mainInData) {
throw new \BadMethodCallException('Cannot unset main currency');
throw new InvalidFieldException(InvalidFieldException::CSTR_DEFAULT_REQUIRED,
static::MODEL_NAME, 'main',
['main' => true], false);
}
if ($updateMain) {
$main->setMain(false);
......@@ -79,7 +82,9 @@ class CurrencyAPI extends APIRefHelper
$model = $this->dao->read(static::MODEL_NAME, $i);
if ($model != null) {
if ($model->isMain()) {
throw new \BadMethodCallException('Cannot delete main currency');
throw new InvalidFieldException(InvalidFieldException::CSTR_DEFAULT_REQUIRED,
static::MODEL_NAME, 'main', ['main' => true],
false);
}
$references[] = $model;
}
......
......@@ -27,19 +27,24 @@ namespace Pasteque\Server\Exception;
class InvalidFieldException extends \Exception implements PastequeException
{
/** The field is read only. */
const CSTR_READ_ONLY = 'read_only_field';
const CSTR_READ_ONLY = 'ReadOnly';
/** The value must be unique. */
const CSTR_UNIQUE = 'unique_value';
const CSTR_UNIQUE = 'UniqueValue';
/** The field is a reference to an other record but this record
* was not found. */
const CSTR_ASSOCIATION_NOT_FOUND = 'association_not_found';
const CSTR_ASSOCIATION_NOT_FOUND = 'AssociationNotFound';
/** The field is null but it cannot be. */
const CSTR_NOT_NULL = 'not_null';
const CSTR_NOT_NULL = 'NotNull';
/** The field is not an array but it must be. */
const CSTR_ARRAY = 'array';
const CSTR_ARRAY = 'ArrayRequired';
/** The field is a reference to a cash session but this session
* must be opened. */
const CSTR_OPENED_CASH = 'opened_cash_required';
const CSTR_OPENED_CASH = 'OpenedCashRequired';
/**
* The field marks the record as the default one and there would be no
* default anymore.
*/
const CSTR_DEFAULT_REQUIRED = 'DefaultRequired';
private $constraint;
private $class;
......@@ -53,27 +58,7 @@ class InvalidFieldException extends \Exception implements PastequeException
$this->field = $field;
$this->id = $id;
$this->value = $value;
switch ($constraint) {
case static::CSTR_READ_ONLY:
$msg = sprintf('The field %s of %s is read only (write to update %s with value %s).',
$this->field, $this->class,
json_encode($this->getJsonableId()),
json_encode($this->getJsonableValue()));
break;
case static::CSTR_UNIQUE:
$msg = sprintf('The field %s of %s must be unique (write to update %s with value %s).',
$this->field, $this->class,
json_encode($this->getJsonableId()),
json_encode($this->getJsonableValue()));
break;
case static::CSTR_OPENED_CASH:
$msg = sprintf('The associated associated cash session in %s of %s must be opened (tried to write %s with value %s).',
$this->field, $this->class,
json_encode($this->getJsonableid()),
json_encode($this->getJsonableValue()));
break;
}
parent::__construct($msg);
parent::__construct();
}
public function getConstraint() {
......@@ -114,13 +99,12 @@ class InvalidFieldException extends \Exception implements PastequeException
public function toStruct() {
return [
'error' => 'InvalidFieldException',
'error' => 'InvalidField',
'constraint' => $this->constraint,
'class' => $this->class,
'field' => $this->field,
'id' => $this->getJsonableId(),
'key' => $this->getJsonableId(),
'value' => $this->getJsonableValue(),
'message' => $this->message,
];
}
}
......@@ -27,11 +27,9 @@ namespace Pasteque\Server\Exception;
class InvalidRecordException extends \Exception implements PastequeException
{
/** The record is read only. */
const CSTR_READ_ONLY = 'read_only_record';
const CSTR_READ_ONLY = 'ReadOnly';
/** The record is generated and cannot be written directly. */
const CSTR_GENERATED = 'generated_record';
/** The requested record to update was not found at all. */
const CSTR_MAIN_RECORD_NOT_FOUND = 'main_record_not_found';
const CSTR_GENERATED = 'Generated';
private $constraint;
private $class;
......@@ -41,25 +39,7 @@ class InvalidRecordException extends \Exception implements PastequeException
$this->constraint = $constraint;
$this->class = $class;
$this->id = $id;
switch ($constraint) {
case static::CSTR_READ_ONLY:
$msg = sprintf('%s is read only (tried to update id %s).',
$this->class, json_encode($this->getJsonableId()));
break;
case static::CSTR_GENERATED:
$msg = sprintf('%s id %s is generated and cannot be written directly.',
$this->class, json_encode($this->getJsonableId()));
break;
case static::CSTR_MAIN_RECORD_NOT_FOUND:
$msg = sprintf('%s id %s was not found.',
$this->class, json_encode($this->getJsonableId()));
break;
default:
$msg = sprintf('Invalid record error on %s id %s.',
$this->class, json_encode($this->getJsonableId()));
break;
}
parent::__construct($msg);
parent::__construct();
}
public function getConstraint() {
......@@ -84,11 +64,10 @@ class InvalidRecordException extends \Exception implements PastequeException
public function toStruct() {
return [
'error' => 'InvalidRecordException',
'error' => 'InvalidRecord',
'constraint' => $this->constraint,
'class' => $this->class,
'id' => $this->getJsonableId(),
'message' => $this->message,
'key' => $this->getJsonableId(),
];
}
}
......@@ -32,9 +32,7 @@ class RecordNotFoundException extends \Exception implements PastequeException
public function __construct($class, $id) {
$this->class = $class;
$this->id = $id;
$msg = sprintf("%s with id %s was not found",
$this->class, $this->getJsonableId());
parent::__construct($msg);
parent::__construct();
}
public function getClass() {
......@@ -55,10 +53,9 @@ class RecordNotFoundException extends \Exception implements PastequeException
public function toStruct() {
return [
'error' => 'RecordNotFoundException',
'error' => 'RecordNotFound',
'class' => $this->class,
'id' => $this->getJsonableId(),
'message' => $this->message,
'key' => $this->getJsonableId(),
];
}
}
......@@ -78,8 +78,12 @@ abstract class DoctrineMainModel extends DoctrineModel
$refField = static::getReferenceKey();
$conditions = [];
if (!is_array($refField)) {
$sRef = $ref;
if (is_array($ref) && array_key_exists($refField, $ref)) {
$sRef = $ref[$refField];
}
$conditions[] = new DAOCondition(static::getReferenceKey(),
'=', $ref);
'=', $sRef);
} else {
foreach ($refField as $fieldName) {
if (!array_key_exists($fieldName, $ref)) {
......@@ -96,4 +100,38 @@ abstract class DoctrineMainModel extends DoctrineModel
return null;
}
}
/**
* Extract the loading key to pass to load() from a struct.
* @param array $struct
* @return array An associative array to pass to load().
*/
public static function getLoadKey($struct) {
$refField = static::getReferenceKey();
if (!is_array($refField)) {
if (array_key_exists($refField, $struct)) {
return [$refField => $struct[$refField]];
}
return null;
} else {
$key = [];
foreach ($refField as $fieldName) {
if (!array_key_exists($fieldName, $ref)) {
return null;
}
$key[$fieldName] = $struct[$fieldName];
}
return $key;
}
}
/**
* Combines getLoadKey with load. It doesn't merge.
* @param array $struct
* @param $dao The dao that will look for the record.
* @return A record linked to the DAO, or null.
*/
public static function structLoad($struct, $dao) {
return static::load(static::getLoadKey($struct), $dao);
}
}
......@@ -22,6 +22,7 @@
namespace Pasteque\Server;
use \Pasteque\Server\API\CurrencyAPI;
use \Pasteque\Server\Exception\InvalidFieldException;
use \Pasteque\Server\Model\Currency;
use \Pasteque\Server\System\DAO\DAOFactory;
use \PHPUnit\Framework\TestCase;
......@@ -83,7 +84,6 @@ class CurrencyAPITest extends TestCase
}
public function testDeleteMain() {
$this->expectException(\BadMethodCallException::class);
$main = new Currency();
$main->setReference('main');
$main->setLabel('Main');
......@@ -94,11 +94,18 @@ class CurrencyAPITest extends TestCase
$this->dao->write($main);
$this->dao->write($other);
$this->dao->commit();
$this->api->delete($main->getId());
$exceptionThrown = false;
try {
$this->api->delete($main->getId());
} catch (InvalidFieldException $e) {
$exceptionThrown = true;
$this->assertEquals(InvalidFieldException::CSTR_DEFAULT_REQUIRED,
$e->getConstraint());
}
$this->assertTrue($exceptionThrown);
}
public function testUnsetMain() {
$this->expectException(\BadMethodCallException::class);
$main = new Currency();
$main->setReference('main');
$main->setLabel('Main');
......@@ -110,6 +117,14 @@ class CurrencyAPITest extends TestCase
$this->dao->write($other);
$this->dao->commit();
$main->setMain(false);
$this->api->write($main);
$exceptionThrown = false;
try {
$this->api->write($main);
} catch (InvalidFieldException $e) {
$exceptionThrown = true;
$this->assertEquals(InvalidFieldException::CSTR_DEFAULT_REQUIRED,
$e->getConstraint());
}
$this->assertTrue($exceptionThrown);
}
}
<?php
// Pasteque server testing
//
// Copyright (C)
// 2012 Scil (http://scil.coop)
// 2017 Karamel, Association Pastèque (karamel@creativekara.fr, https://pasteque.org)
//
// This file is part of Pasteque.
//
// Pasteque is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pasteque is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pasteque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server;
use \Pasteque\Server\Model\Currency;
use \Pasteque\Server\System\Login;
use \Pasteque\Server\System\DAO\DAOCondition;
use \Pasteque\Server\System\DAO\DAOFactory;
use \PHPUnit\Framework\TestCase;
require_once(dirname(dirname(__FILE__)) . "/common_load.php");
class HttpCurrencyTest extends TestCase
{
private $curl;
private static $token;
private $dao;
private $curr;
public static function setUpBeforeClass(): void {
static::$token = obtainToken();
}
public static function tearDownAfterClass(): void {
}
protected function setUp(): void {
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token]);
global $dbInfo;
$this->dao = DAOFactory::getDAO($dbInfo, ['debug' => true]);
$this->curr = new Currency();
$this->curr->merge(['reference' => 'Reference', 'label' => 'label',
'main' => true], $this->dao);
$this->dao->write($this->curr);
$this->dao->commit();
}
protected function tearDown(): void {
curl_close($this->curl);
foreach ([Currency::class] as $class) {
$all = $this->dao->search($class);
foreach($all as $record) {
$this->dao->delete($record);
}
}
$this->dao->commit();
$this->dao->close();
}
public function testPostNew() {
$curr = new Currency();
$curr->merge(['reference' => 'New ref', 'label' => 'New label'],
$this->dao);
$struct = $curr->toStruct();
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/currency'));
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($struct));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token,
'Content-Type: application/json']);
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$dbCurr = $this->dao->search(Currency::class,
new DAOCondition('reference', '=', $struct['reference']));
$this->assertEquals(1, count($dbCurr));
}
public function testPostEdit() {
$this->curr->merge(['reference' => 'New ref', 'label' => 'New label'],
$this->dao);
$struct = $this->curr->toStruct();
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/currency'));
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($struct));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token,
'Content-Type: application/json']);
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$dbCurr = $this->dao->search(Currency::class);
$this->assertEquals(1, count($dbCurr));
$this->assertEquals('New ref', $dbCurr[0]->getReference());
$this->assertEquals('New label', $dbCurr[0]->getLabel());
}
public function testPostRefExisting() {
$curr = new Currency();
$curr->merge(['reference' => 'Reference', 'label' => 'New label'],
$this->dao);
$struct = $curr->toStruct();
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/currency'));
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($struct));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token,
'Content-Type: application/json']);
$resp = curl_exec($this->curl);
$this->assertEquals(400, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$jsResp = json_decode($resp, true);
$this->assertNotEquals(false, $jsResp);
$this->assertEquals('InvalidField', $jsResp['error']);
$this->assertEquals('UniqueValue', $jsResp['constraint']);
$this->assertEquals(Currency::class, $jsResp['class']);
$this->assertEquals('reference', $jsResp['field']);
$this->assertEquals('Reference', $jsResp['value']);
$this->assertEquals('Reference', $jsResp['key']['reference']);
}
public function testPutOk() {
$curr = new Currency();
$curr->merge(['reference' => 'New ref', 'label' => 'New label'],
$this->dao);
$struct = $curr->toStruct();
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($this->curl, CURLOPT_URL,
apiUrl(sprintf('api/currency/%s',
urlencode($struct['reference']))));
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($struct));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token,
'Content-Type: application/json']);
$resp = curl_exec($this->curl);
$this->assertEquals(200, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$dbCurr = $this->dao->search(Currency::class,
new DAOCondition('reference', '=', $struct['reference']));
$this->assertEquals(1, count($dbCurr));
}
public function testPutId() {
$curr = new Currency();
$curr->merge(['reference' => 'newref', 'label' => 'New label'],
$this->dao);
$struct = $curr->toStruct();
$struct['id'] = 1;
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($this->curl, CURLOPT_URL, apiUrl('api/currency/newref'));
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($struct));
curl_setopt($this->curl, CURLOPT_HTTPHEADER,
[Login::TOKEN_HEADER . ': ' . static::$token,
'Content-Type: application/json']);
$resp = curl_exec($this->curl);
$this->assertEquals(400, curl_getinfo($this->curl, CURLINFO_HTTP_CODE));
$this->markTestIncomplete('Test response message, not available easily with curl');
}
public function testPutRefMismatch() {
$curr = new Currency();