Implement Matrix as data backend

This commit is contained in:
Ben 2021-09-19 16:55:30 +02:00
parent 1f14932b28
commit 01212543fe
Signed by: ben
GPG key ID: 0F54A7ED232D3319
14 changed files with 338 additions and 156 deletions

8
composer.lock generated
View file

@ -12,12 +12,12 @@
"source": {
"type": "git",
"url": "https://github.com/bziemons/matrix-php-sdk.git",
"reference": "54aa76a82237b1abea6b6d7157b03b39d207846c"
"reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/54aa76a82237b1abea6b6d7157b03b39d207846c",
"reference": "54aa76a82237b1abea6b6d7157b03b39d207846c",
"url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/f0d759e56bf7013c1b0d9936acd1406ebcfcecb9",
"reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9",
"shasum": ""
},
"require": {
@ -73,7 +73,7 @@
"support": {
"source": "https://github.com/bziemons/matrix-php-sdk/tree/feature/guzzle7-update"
},
"time": "2021-09-18T10:11:20+00:00"
"time": "2021-09-19T12:54:20+00:00"
},
{
"name": "guzzlehttp/guzzle",

View file

@ -45,8 +45,7 @@ class TicketApiController extends ApiController
public function create(string $title, string $content): DataResponse
{
return new DataResponse($this->service->create($title, $content,
$this->userId));
return new DataResponse($this->service->create($title, $content, $this->userId));
}
public function update(int $id, string $title, string $content): DataResponse

View file

@ -8,12 +8,14 @@ use OCP\AppFramework\Db\Entity;
class MatrixUser extends Entity implements JsonSerializable {
protected $userId;
protected $matrixUser;
protected $matrixToken;
public function jsonSerialize(): array {
return [
'id' => $this->id,
'userId' => $this->userId,
'matrixUser' => $this->matrixUser,
'matrixToken' => $this->matrixToken,
];
}
}

View file

@ -22,20 +22,14 @@ class TicketMapper extends QBMapper
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function find(int $id, string $userId): MatrixTicket
public function find(int $id, MatrixUser $matrixUser): MatrixTicket
{
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling_tickets', 't')
->from('upschooling_tickets')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->innerJoin(
't',
'upschooling_users',
'u',
$qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user'])
)
->andWhere($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId)));
->andWhere($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
return $this->findEntity($qb);
}
@ -43,19 +37,13 @@ class TicketMapper extends QBMapper
* @param string $userId
* @return array
*/
public function findAll(string $userId): array
public function findAll(MatrixUser $matrixUser): array
{
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling_users', 'u')
->where($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId)))
->innerJoin(
'u',
'upschooling_tickets',
't',
$qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user'])
);
->from('upschooling_tickets')
->where($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
return $this->findEntities($qb);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace OCA\UPschooling\Exceptions;
use OC\OCS\Exception;
use OC\OCS\Result;
class RoomNotFoundException extends Exception {
public function __construct()
{
parent::__construct(new Result(null, 404, 'Room not found'));
}
}

View file

@ -29,22 +29,22 @@ class Version000000Date20210918151800 extends SimpleMigrationStep {
]);
$table->addColumn('matrix_room', 'string', [
'notnull' => true,
'length' => 63,
'length' => 200,
]);
$table->addColumn('matrix_assisted_user', 'string', [
'notnull' => true,
'length' => 63,
'length' => 200,
]);
$table->addColumn('matrix_helper_user', 'string', [
'notnull' => false,
'length' => 63,
'length' => 200,
]);
// could be optimized by having a "localUser" with foreign key to users.id,
// but that would create consistency issues
$table->addColumn('status', 'string', [
'notnull' => true,
'length' => 100,
'length' => 63,
]);
$table->addColumn('version', 'integer', [
'notnull' => true,
@ -63,11 +63,15 @@ class Version000000Date20210918151800 extends SimpleMigrationStep {
]);
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 100,
'length' => 200,
]);
$table->addColumn('matrix_user', 'string', [
'notnull' => true,
'length' => 63,
'length' => 200,
]);
$table->addColumn('matrix_token', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->setPrimaryKey(['id']);

View file

@ -3,9 +3,13 @@
namespace OCA\UPschooling\Service;
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
use Aryess\PhpMatrixSdk\Exceptions\ValidationException;
use Aryess\PhpMatrixSdk\MatrixClient;
use Aryess\PhpMatrixSdk\Room;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Exceptions\RoomNotFoundException;
use Psr\Log\LoggerInterface;
class MatrixService
@ -14,58 +18,160 @@ class MatrixService
/** @var LoggerInterface */
private $logger;
/** @var string */
private $channel;
/** @var MatrixClient */
private $client;
/** @var string */
private $token;
/** @var Room */
private $room;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
$this->channel = "#issue:synapse";
$this->client = new MatrixClient("http://synapse:8008");
$this->token = $this->client->login("upschooling", "secret");
// try {
// $this->room = $this->client->createRoom($this->channel, false, array());
// } catch (MatrixException $createE) {
// try {
// $this->room = $this->client->joinRoom($this->channel);
// } catch (MatrixException $e) {
// $this->logger->error(
// "Could not create room ".$this->channel,
// array('exception' => $e, 'causedBy' => $createE)
// );
// }
// }
}
private $registrationSecret = "~d9fJpPKDZIV67A7=tPCvok:=fTBLV;MFf=9FRxtAazW@-GwSo";
/**
* Sets a property on a channel.
*
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
* @param array $content the contents as a JSON serializable array.
* @throws MatrixRequestException
* @throws MatrixHttpLibException
* @throws ValidationException
* @throws MatrixException
*/
public function setProperty(string $eventType, array $content)
public function __construct(LoggerInterface $logger)
{
$this->room->sendStateEvent($eventType, $content);
$this->logger = $logger;
$serverUrl = "http://synapse:8008";
$user = "upschooling";
$this->client = new MatrixClient($serverUrl);
$this->client->login($user, "secret", true);
$this->logger->debug("Logged in as " . $user . " on server " . $serverUrl);
}
/**
* @param string $roomId a room id of an existing and joined room.
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
* @param array $content the contents as a JSON serializable array.
* @throws RoomNotFoundException
* @throws MatrixException
*/
public function setProperty(string $roomId, string $eventType, array $content)
{
$room = $this->findRoom($roomId);
$room->sendStateEvent($eventType, $content);
$this->logger->debug(
"Set property " . $eventType . " on room " . $roomId,
array("room" => $roomId, "key" => $eventType, "value" => $content)
);
}
/**
* @param string $roomId a room id of an existing and joined room.
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
* @return array the contents of the room state.
* @throws MatrixException|MatrixRequestException
* @throws MatrixException
* @throws RoomNotFoundException
*/
public function getProperty(string $eventType): array
public function getProperty(string $roomId, string $eventType): array
{
// first parameter should be $this->room->roomId
return $this->client->api()->getStateEvent($this->channel, $eventType);
$this->findRoom($roomId); // make sure the room exists/is joined
$content = $this->client->api()->getStateEvent($roomId, $eventType);
$this->logger->debug(
"Got property " . $eventType . " from room " . $roomId,
array("room" => $roomId, "key" => $eventType, "value" => $content)
);
return $content;
}
/**
* @param string $roomId a room id of an existing and joined room.
* @throws RoomNotFoundException
* @returns int the origin server timestamp of the most recent event.
*/
public function getLastEventDate(string $roomId): int
{
$room = $this->findRoom($roomId);
$events = $room->getEvents();
if (count($events) === 0) {
$this->logger->debug("Did not have any events for room " . $roomId);
return 0;
} else {
$timestamp = array_get($events[0], 'origin_server_ts', 1);
if ($timestamp === 1) {
$this->logger->debug("Could not find origin_server_ts in last event of room " . $roomId);
} else {
$this->logger->debug("Last event in room " . $roomId . " was at " . $timestamp);
}
return $timestamp;
}
}
/**
* Registers a new matrix user with the local matrix server.
*
* @throws MatrixException
* @throws MatrixHttpLibException
* @throws MatrixRequestException
* @return MatrixUser user object without id or Nextcloud user id set.
*/
public function registerNewUser(): MatrixUser
{
$nonceResponse = $this->client->api()->send(
'GET',
'/register',
null,
[],
[],
'/_synapse/admin/v1', true
);
$randUsername = trim(str_replace(["/", "+"], ".", base64_encode(random_bytes(6))), "=");
$username = "upschooling_" . $randUsername;
$password = base64_encode(random_bytes(32));
$hmacData = $nonceResponse["nonce"] . "\x00" . $username . "\x00" . $password . "\x00notadmin";
$hmac = hash_hmac("sha1", $hmacData, $this->registrationSecret, false);
$registrationResponse = $this->client->api()->send(
'POST',
'/register',
array(
"nonce" => $nonceResponse["nonce"],
"username" => $username,
"password" => $password,
"displayname" => "UPschooling Support User " . $randUsername,
"admin" => false,
"mac" => $hmac,
),
[],
[],
'/_synapse/admin/v1', true
);
$matrixUser = new MatrixUser();
$matrixUser->setMatrixUser($registrationResponse["user_id"]);
$matrixUser->setMatrixToken($registrationResponse["access_token"]);
$this->logger->debug("Created a new user: " . $matrixUser->getMatrixUser());
return $matrixUser;
}
/**
* @param string $roomId a room id of an existing and joined room.
* @throws RoomNotFoundException
* @returns Room the room object, if found.
*/
private function findRoom(string $roomId): Room
{
foreach ($this->client->getRooms() as $room) {
if ($room->getRoomId() === $roomId) {
$this->logger->debug("Found room " . $roomId . " on matrix client");
return $room;
}
}
$this->logger->error("Room " . $roomId . " was not found on matrix client");
throw new RoomNotFoundException();
}
/**
* Creates a new room.
*
* @throws MatrixException
* @return string the room id of the newly created room.
*/
public function createRoom(): string
{
$roomId = $this->client->createRoom()->getRoomId();
$this->logger->debug("Created a new room: " . $roomId);
return $roomId;
}
}

View file

@ -1,6 +0,0 @@
<?php
namespace OCA\UPschooling\Service;
class NoteNotFound extends \Exception {
}

View file

@ -2,76 +2,113 @@
namespace OCA\UPschooling\Service;
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
use Exception;
use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Db\TicketMapper;
use OCA\UPschooling\Db\UserMapper;
use OCA\UPschooling\Exceptions\RoomNotFoundException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
class TicketService {
/** @var TicketMapper */
private $mapper;
/** @var MatrixService */
private $matrix;
public function __construct(MatrixService $matrix, TicketMapper $mapper) {
/** @var TicketMapper */
private $ticketMapper;
/** @var UserMapper */
private $userMapper;
public function __construct(MatrixService $matrix, TicketMapper $ticketMapper, UserMapper $userMapper) {
$this->matrix = $matrix;
$this->mapper = $mapper;
$this->ticketMapper = $ticketMapper;
$this->userMapper = $userMapper;
}
public function findAll(string $userId): array {
return $this->mapper->findAll($userId);
$dbTickets = $this->ticketMapper->findAll($this->getOrCreateUser($userId));
return array_map(function ($ticket) { return $this->resolveTicket($ticket); }, $dbTickets);
}
private function handleException(Exception $e): void {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new NoteNotFound($e->getMessage());
} else {
throw $e;
}
public function find($id, $userId): array {
return $this->resolveTicket($this->ticketMapper->find($id, $this->getOrCreateUser($userId)));
}
public function find($id, $userId) {
try {
return $this->mapper->find($id, $userId);
// in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions
// into service related exceptions so controllers and service users
// have to deal with only one type of exception
} catch (Exception $e) {
$this->handleException($e);
}
}
public function create($title, $content, $userId) {
$note = new MatrixTicket();
$note->setTitle($title);
$note->setContent($content);
$note->setUserId($userId);
return $this->mapper->insert($note);
public function create($title, $content, $userId): array {
$matrixUser = $this->getOrCreateUser($userId);
// FIXME: room must be joined on the remote support-platform Nextcloud
$roomId = $this->matrix->createRoom();
$this->matrix->setProperty($roomId, "upschooling.ticket", array(
// id is to be set from the remote support-platform Nextcloud
"title" => $title,
"description" => $content,
));
$ticket = new MatrixTicket();
$ticket->setMatrixRoom($roomId);
$ticket->setMatrixAssistedUser($matrixUser->getMatrixUser());
$ticket->setStatus("open");
$ticket->setVersion(1);
return $this->resolveTicket($this->ticketMapper->insert($ticket));
}
public function update($id, $title, $content, $userId) {
try {
$note = $this->mapper->find($id, $userId);
$note->setTitle($title);
$note->setContent($content);
return $this->mapper->update($note);
} catch (Exception $e) {
$this->handleException($e);
}
throw new Exception("Not implemented");
}
public function delete($id, $userId) {
throw new Exception("Not implemented");
}
/**
* @param MatrixTicket $ticket the database object.
* @return array a JSON serializable representation of the resolved ticket, for the frontend.
*/
private function resolveTicket(MatrixTicket $ticket): array
{
try {
$note = $this->mapper->find($id, $userId);
$this->mapper->delete($note);
return $note;
} catch (Exception $e) {
$this->handleException($e);
$matrixTicketContent = $this->matrix->getProperty($ticket->getMatrixRoom(), "upschooling.ticket");
$ticketId = array_get($matrixTicketContent, "id", $ticket->getId());
$title = array_get($matrixTicketContent, "title", "Untitled");
$description = array_get($matrixTicketContent, "description", "");
$lastModified = $this->matrix->getLastEventDate($ticket->getMatrixRoom());
return array(
'ticketId' => $ticketId,
'status' => $ticket->getStatus(),
'lastModified' => $lastModified,
'title' => $title,
'description' => $description,
);
} catch (MatrixException | RoomNotFoundException $e) {
return array(
'ticketId' => $ticket->getId(),
'status' => 'error',
);
}
}
/**
* @param $userId string Nextcloud user id
* @throws MatrixException
* @throws MatrixHttpLibException
* @throws MatrixRequestException
* @throws MultipleObjectsReturnedException
* @throws \OCP\DB\Exception
*/
private function getOrCreateUser(string $userId): MatrixUser
{
try {
return $this->userMapper->find($userId);
} catch (DoesNotExistException $e) {
$matrixUser = $this->matrix->registerNewUser();
$matrixUser->setUserId($userId);
$this->userMapper->insert($matrixUser);
return $matrixUser;
}
}
}

View file

@ -9,7 +9,7 @@ DIR="${0%/*}"
podman rm -if synapse
podman rm -if nextcloud
podman run -d --name=nextcloud -p 8080:80 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud
podman run -d --name=nextcloud -p 8080:80 -p 8008:8008 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud
podman exec nextcloud chown -R 33 /var/www/html/custom_apps
"$DIR/podman-reown.sh"
podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'

View file

@ -38,6 +38,7 @@ export default {
*/
currentTicket: undefined,
ticketsFetched: false,
tickets: [],
}
},
@ -45,19 +46,43 @@ export default {
$route: 'fetchTickets',
},
created() {
this.createExampleContent()
this.fetchTickets()
},
methods: {
createExampleContent() {
// this obviously shouldn't survive version 1.0
axios.post(
'api/v1/tickets',
{ title: 'Erstes Ticket', content: 'Erstes Beispiel' },
{ headers: { 'Content-Type': 'application/json', Accept: 'application/json' } },
).then((response) => {
if (this.ticketsFetched === false) {
this.tickets.push(response.data)
}
}).catch(console.error)
axios.post(
'api/v1/tickets',
{ title: 'Zweites Ticket', content: 'Zweites Beispiel' },
{ headers: { 'Content-Type': 'application/json', Accept: 'application/json' } },
).then((response) => {
if (this.ticketsFetched === false) {
this.tickets.push(response.data)
}
}).catch(console.error)
},
fetchTickets() {
axios.get(
'api/v1/tickets',
{ headers: { Accept: 'application/json' } }
).then((response) => {
if (Array.isArray(response.data)) {
this.ticketsFetched = true
if (response.data.length !== 0) {
this.tickets.push(response.data)
this.tickets = response.data
console.debug(this.tickets) // FIXME
} else {
console.warn('Empty ticket list :(')
console.debug('Empty ticket list :(')
}
} else {
console.error('API did not return array: ', response)
@ -69,7 +94,7 @@ export default {
console.debug('upschooling', 'saveTicket', ticketId, data)
},
openTicket(ticketId) {
this.currentTicket = this.tickets.find((obj) => obj.id === ticketId)
this.currentTicket = this.tickets.find((obj) => obj.ticketId === ticketId)
},
deselectTicket() {
this.currentTicket = null

View file

@ -9,13 +9,13 @@
</button>
</div>
<h2>Ticket "ding"</h2>
<KeyValueTable :data-rows="{Name: ticket.title, Status: ticket.status, Ablaufdatum: '<<gestern>>'}" />
<KeyValueTable :data-rows="{Id: ticket.ticketId, Name: ticket.title, Status: ticket.status, Geaendert: toLocaleDate(ticket.lastModified)}" />
<br>
<label for="description">{{ t('upschooling', 'Beschreibung') }}</label>
<textarea id="description" v-model.trim.lazy="description" rows="15" />
<br>
<button @click="save">
Speichern
{{ t('upschooling', 'Speichern') }}
</button>
<hr>
<div class="placeholder" />
@ -41,8 +41,13 @@ export default {
}
},
methods: {
toLocaleDate(timestamp) {
const date = new Date(timestamp)
return date.toLocaleString()
},
save() {
this.$emit('save-ticket', this.ticket.id, {}) // TODO: give it only the changed data
this.$emit('save-ticket', this.ticket.ticketId, {}) // TODO: give it only the changed data
},
back() {

View file

@ -7,32 +7,32 @@
<th id="headerName" class="column-name">
<div id="headerName-container">
<a class="name sort columntitle" data-sort="name">
<span>Titel (#Ticket-Nummer)</span>
<span>{{ t('upschooling', 'Titel (#Ticket-Nummer)') }}</span>
<span class="sort-indicator icon-triangle-n" />
</a>
</div>
</th>
<th id="headerStatus" class="column-status">
<a class="status sort columntitle" data-sort="status">
<span>Status</span>
<span>{{ t('upschooling', 'Status') }}</span>
<span class="sort-indicator hidden icon-triangle-s" />
</a>
</th>
<th id="headerDate" class="column-mtime">
<a id="modified" class="columntitle" data-sort="mtime">
<span>Geändert</span>
<span>{{ t('upschooling', 'Zuletzt Geändert') }}</span>
<span class="sort-indicator hidden icon-triangle-s" />
</a>
</th>
</tr>
</thead>
<tbody id="tickettbody">
<tr v-for="item in tickets" :key="item.id">
<tr v-for="item in tickets" :key="item.ticketId">
<td class="filename ui-draggable ui-draggable-handle">
<a class="name" :href="'#ticket-' + item.id" @click="openTicket(item.id)">
<a class="name" :href="'#ticket-' + item.ticketId" @click="openTicket(item.ticketId)">
<span class="nametext">
<span class="innernametext">{{ item.title }}</span>
<span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.id }})</span>
<span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.ticketId }})</span>
</span>
</a>
</td>
@ -42,11 +42,10 @@
<td class="date">
<span
class="modified live-relative-timestamp"
title=""
data-timestamp="1628157115000"
style="color:rgb(81,81,81)"
data-original-title="5. August 2021 11:51">
vor 16 Tagen <!-- ToDo -->
:title="toLocaleDate(item.lastModified)"
:data-timestamp="item.lastModified"
style="color:rgb(81,81,81)">
{{ toLocaleDate(item.lastModified) }}
</span>
</td>
</tr>
@ -66,6 +65,10 @@ export default {
},
},
methods: {
toLocaleDate(timestamp) {
const date = new Date(timestamp)
return date.toLocaleString()
},
openTicket(ticketId) {
this.$emit('open-ticket', ticketId)
},

View file

@ -4,8 +4,8 @@ namespace Unit\Service;
use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\TicketMapper;
use OCA\UPschooling\Db\UserMapper;
use OCA\UPschooling\Service\MatrixService;
use OCA\UPschooling\Service\NoteNotFound;
use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\Db\DoesNotExistException;
use PHPUnit\Framework\TestCase;
@ -20,17 +20,23 @@ class NoteServiceTest extends TestCase {
private $matrixService;
/** @var TicketMapper */
private $mapper;
private $ticketMapper;
/** @var UserMapper */
private $userMapper;
/** @var string */
private $userId = 'john';
public function setUp(): void {
$this->mapper = $this->getMockBuilder(TicketMapper::class)
$this->ticketMapper = $this->getMockBuilder(TicketMapper::class)
->disableOriginalConstructor()
->getMock();
$this->userMapper = $this->getMockBuilder(UserMapper::class)
->disableOriginalConstructor()
->getMock();
$this->matrixService = new MatrixService(new NullLogger());
$this->service = new TicketService($this->matrixService, $this->mapper);
$this->service = new TicketService($this->matrixService, $this->ticketMapper, $this->userMapper);
}
public function testUpdate() {
@ -40,7 +46,7 @@ class NoteServiceTest extends TestCase {
'title' => 'yo',
'content' => 'nope'
]);
$this->mapper->expects($this->once())
$this->ticketMapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->returnValue($note));
@ -49,7 +55,7 @@ class NoteServiceTest extends TestCase {
$updatedNote = MatrixTicket::fromRow(['id' => 3]);
$updatedNote->setTitle('title');
$updatedNote->setContent('content');
$this->mapper->expects($this->once())
$this->ticketMapper->expects($this->once())
->method('update')
->with($this->equalTo($updatedNote))
->will($this->returnValue($updatedNote));
@ -60,9 +66,9 @@ class NoteServiceTest extends TestCase {
}
public function testUpdateNotFound() {
$this->expectException(NoteNotFound::class);
// $this->expectException(NoteNotFound::class);
// test the correct status code if no note is found
$this->mapper->expects($this->once())
$this->ticketMapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->throwException(new DoesNotExistException('')));