diff --git a/CHANGELOG.md b/CHANGELOG.md index b50d2ec..be329b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [2.3.0] - 2026-06-10 +- Added [#78](https://github.com/zammad/zammad-api-client-php/pull/78) - Ticket linking support (Link resource). +- Added [#59](https://github.com/zammad/zammad-api-client-php/issues/59) - Admin-scoped tag support. +- Added [#126](https://github.com/zammad/zammad-api-client-php/pull/126) - Configurable test timeout via `ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_TIMEOUT` and `connection_options` support in HTTPClient. +- Fixed [#52](https://github.com/zammad/zammad-api-client-php/issues/52) - `getAttachmentContent()` to return string instead of Stream object. +- Added [#43](https://github.com/zammad/zammad-api-client-php/issues/43) - `sort_by` and `order_by` parameters for `search()`. +- Fixed [#77](https://github.com/zammad/zammad-api-client-php/issues/77) - `getID()` to handle null values and always return a string. + +## [2.2.3] - 2026-06-08 +- Fixed [#64](https://github.com/zammad/zammad-api-client-php/issues/64) - Use `From` header instead of deprecated `X-On-Behalf-Of`. + ## [2.2.2] - 2026-04-27 - Fix PHP deprecation warnings. diff --git a/README.md b/README.md index fd132e7..40c25cf 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ Additionally you can have a look at the REST interface documentation of Zammad: * [Ticket priorities](https://docs.zammad.org/en/latest/api/ticket-priority.html) * [Ticket states](https://docs.zammad.org/en/latest/api/ticket-state.html) * [Tags](https://docs.zammad.org/en/latest/api/tags.html) -* [Linking Tickets](https://docs.zammad.org/en/latest/api/ticket/links.html) + * [Tag list](https://docs.zammad.org/en/latest/api/ticket/tags.html#administration-scope) + * [Linking Tickets](https://docs.zammad.org/en/latest/api/ticket/links.html) #### Fetching a ticket's articles If you already have a ticket object, you can easily fetch its articles: @@ -99,6 +100,8 @@ $attachment_content = $ticket_article->getAttachmentContent(23); In the above example 23 is the ID of the attachment. This ID can be found within the `attachments` array of the ticket article data. Usually you want to loop over this array to fetch the content of all attachments. +`getAttachmentContent()` returns the attachment content as a string, ready to use. + ### Updating Resource objects If you fetched a `Resource` object and changed some values, you have to send your changes to Zammad. You do this with a simple call: ```php @@ -294,7 +297,7 @@ If you want Zammad to execute an API call on behalf of another user than the one ```php $client->setOnBehalfOfUser('myuser'); ``` -Any API call after above code will use this setting. If you want to return to using the user you used for authentication, call: +This sets the `From` HTTP header. Any API call after above code will use this setting. If you want to return to using the user you used for authentication, call: ```php $client->unsetOnBehalfOfUser(); ``` @@ -323,9 +326,10 @@ $client->resource( ResourceType::TICKET ); | ORGANIZATION|✔|✔|✔|✔|✔|–|–|✔| | GROUP|✔|✔|–|✔|✔|–|–|–| | USER|✔|✔|✔|✔|✔|–|–|✔| -| TAG|✔|–|✔|–|–|✔|✔|–| +| TAG|✔|✔|✔|✔|✔|✔|✔|–| | LINK|✔|–|–|–|–|✔|✔|–| + ## Publishing 1. Add release to [CHANGELOG.md](CHANGELOG.md) @@ -335,3 +339,24 @@ $client->resource( ResourceType::TICKET ); ## Contributing Bug reports and pull requests are welcome on [GitHub](https://github.com/zammad/zammad-api-client-php). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. + +### Running tests + +Tests require a running Zammad instance with API access enabled. Set the following environment variables: + +| Variable | Required | Default | Description | +|---|---|---|---| +| `ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL` | Yes | — | URL to Zammad (e.g. `http://localhost:3000/`) | +| `ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME` | Yes | — | Username for authentication | +| `ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD` | Yes | — | Password for authentication | +| `ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_TIMEOUT` | No | `30` | Request timeout in seconds | + +**Note:** Only username/password authentication is supported for tests. + +```bash +ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL="http://localhost:3000/" \ +ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME="admin@example.com" \ +ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD="test" \ +ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_TIMEOUT=120 \ +vendor/bin/phpunit +``` diff --git a/SECURITY.asc b/SECURITY.asc new file mode 100644 index 0000000..c24056e --- /dev/null +++ b/SECURITY.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZVsi2RYJKwYBBAHaRw8BAQdAIm/0t+RboVPq5syrc0n9hP3UPH7xok7mNCqM +5R39oZi0JVphbW1hZCBTZWN1cml0eSA8c2VjdXJpdHlAemFtbWFkLmNvbT6ImQQT +FgoAQRYhBARIHz68FJQ7lF5Ox7snHWG50ZiEBQJlWyLZAhsDBQkSzAMABQsJCAcC +AiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELsnHWG50ZiEM+MBAMMdppJHzPNRdgke +bv7+z591+LrQqsKJUBUHjlujsxrbAQCF9RRf2CSTaF2SBD9vrGxdL58Bb/AVs1t6 +ZX/Xf/ozDLg4BGVbItkSCisGAQQBl1UBBQEBB0DtyQW5YnpS1MQ+umPKax706r+R +RJZRO63fma5e+rhaKgMBCAeIfgQYFgoAJhYhBARIHz68FJQ7lF5Ox7snHWG50ZiE +BQJlWyLZAhsMBQkSzAMAAAoJELsnHWG50ZiE9w8BAKj36yLaf7do05ObiTjpFR5P +iDa6aRHJSWDpdut8Q19jAQCfH1WZ2M/2VK0E03k6zcfc56m+z1gwdkq78dAunte2 +BA== +=GDpl +-----END PGP PUBLIC KEY BLOCK----- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c03fd61 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +## Supported Versions + +Security fixes are provided for the current stable version of this PHP API client only. +Any older version is not supported and needs to be updated first before reporting security issues. + +## Reporting a Vulnerability + +If you've found a security vulnerability in Zammad, +please report the vulnerability exclusively via email +to [security@zammad.com](mailto:security@zammad.com). + +Please do not combine several independent vulnerabilities, +but send a separate mail for each of them instead. + +To send us a secure message, please use [our public key](SECURITY.asc). + +We will get back to you as soon as possible and inform +you about the next steps. Accepted vulnerabilities will +be disclosed via patch level release with accompanying +security advisory. + +### Reporting Process Overview + +- Potential security issues can be reported via [security@zammad.com](mailto:security@zammad.com). +- We evaluate them and provide timely feedback to the reporter. +- There may be security releases created if needed, e.g. [Zammad 6.3.1](https://zammad.com/en/releases/6-3-1). +- We publish security advisories for every acknowledged issue via [GitHub Security Advisories](https://github.com/zammad/zammad/security/advisories). +- After their publication, we request CVE identifiers to be assigned to the advisories. + +### Rewards + +Every first reporter of a vulnerability may be credited +in the related security advisory. + +Zammad does not offer financial compensation through a +security bounty program. + +## Security Measures in Development Workflow + +### Dependency Management + +Dependencies are managed via [Composer](https://getcomposer.org/). +You can check for known security vulnerabilities in dependencies by running: + +```bash +composer audit +``` diff --git a/examples/tag_admin.php b/examples/tag_admin.php new file mode 100644 index 0000000..6150af9 --- /dev/null +++ b/examples/tag_admin.php @@ -0,0 +1,68 @@ + 'https://my.zammad.com', + 'username' => 'my-username', + 'password' => 'my-password', +]); + +// List all tags (administration scope) +$tags = $client->resource(ResourceType::TAG)->all(); +echo "All tags:\n"; +foreach ($tags as $tag) { + echo sprintf( + " ID: %s, Name: %s, Count: %s\n", + $tag->getID(), + $tag->getValue('name'), + $tag->getValue('count') + ); +} + +// Create a new tag +$tag = $client->resource(ResourceType::TAG); +$tag->setValue('name', 'example-tag'); +$tag->save(); + +echo "\nTag 'example-tag' created.\n"; + +// Find the created tag to get its ID +$tag_id = null; +$tags = $client->resource(ResourceType::TAG)->all(); +foreach ($tags as $t) { + if ($t->getValue('name') === 'example-tag') { + $tag_id = $t->getID(); + break; + } +} + +if ($tag_id) { + echo "Tag ID: $tag_id\n"; + + // Rename the tag (update) + // Set the tag's ID so save() will call update() instead of create() + $tag = $client->resource(ResourceType::TAG); + $tag->setRemoteData(['id' => $tag_id]); + $tag->setValue('name', 'example-tag-renamed'); + $tag->save(); + + echo "Tag renamed to 'example-tag-renamed'.\n"; + + // Delete the tag + $tag = $client->resource(ResourceType::TAG); + $tag->setRemoteData(['id' => $tag_id]); + $tag->delete(); + + echo "Tag deleted.\n"; +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8ba564e..2cd5e9e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,7 @@ + --> diff --git a/src/Client.php b/src/Client.php index 9361112..835195a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -73,9 +73,9 @@ private function request ( $method, $url, array $url_parameters = [], array $opt $options['headers']['Accept'] = 'application/json'; $options['headers']['Content-Type'] = 'application/json; charset=utf-8'; - // Set "on behalf of user" header + // Set "on behalf of user" header (X-On-Behalf-Of was deprecated in Zammad 6.5 in favor of From) if ( !empty($this->on_behalf_of_user) ) { - $options['headers']['X-On-Behalf-Of'] = $this->on_behalf_of_user; + $options['headers']['From'] = $this->on_behalf_of_user; } $http_client_response = $this->http_client->request( $method, $url, $options ); diff --git a/src/HTTPClient.php b/src/HTTPClient.php index ea8f602..08531a7 100644 --- a/src/HTTPClient.php +++ b/src/HTTPClient.php @@ -134,14 +134,22 @@ public function __construct( array $options = [] ) $verifySsl = $options['verify']; } + // Optional: pass additional Guzzle options (e.g. headers, curl options) + $connection_options = []; + if (is_array($options['connection_options'] ?? null)) { + $connection_options = $options['connection_options']; + } + // Execute constructor of base class - parent::__construct([ - 'base_uri' => $this->base_url, - 'timeout' => $timeout, - 'connect_timeout' => $timeout, - 'debug' => $debug, - 'verify' => $verifySsl, - ]); + parent::__construct( + [ + 'base_uri' => $this->base_url, + 'timeout' => $timeout, + 'connect_timeout' => $timeout, + 'debug' => $debug, + 'verify' => $verifySsl, + ] + $connection_options + ); } /** diff --git a/src/Resource/AbstractResource.php b/src/Resource/AbstractResource.php index 5704c5c..60478f9 100644 --- a/src/Resource/AbstractResource.php +++ b/src/Resource/AbstractResource.php @@ -173,11 +173,12 @@ public function clearUnsavedValues() * Gets the ID of the object. * This is a convenience method for getValue('id') * - * @return string ID of this object, if available. + * @return string|null ID of this object as string, or null if not available. */ public function getID() { - return $this->getValue('id'); + $value = $this->getValue('id'); + return $value === null ? null : (string) $value; } /** @@ -382,11 +383,13 @@ private function allWithoutPagination() * @param string $search_term Search term. * @param integer $page Page of objects, optional, if given, $objects_per_page must also be given. * @param integer $objects_per_page Number of objects per page, optional, if given, $page must also be given. + * @param string $sort_by Sort by field name, optional (e.g. 'created_at', 'title', 'number'). + * @param string $order_by Sort order, optional, must be 'asc' or 'desc'. * * @return mixed Returns array of ZammadAPIClient\Resource\... objects * or this object on failure. */ - public function search( $search_term, $page = null, $objects_per_page = null ) + public function search( $search_term, $page = null, $objects_per_page = null, $sort_by = null, $order_by = null ) { if ( !empty( $this->getValues() ) ) { throw new AlreadyFetchedObjectException('Object already contains values, search() not possible, use a new object'); @@ -405,8 +408,12 @@ public function search( $search_term, $page = null, $objects_per_page = null ) throw new \RuntimeException('Parameters page and objects_per_page must both be given'); } + if ( isset($order_by) && !in_array( mb_strtolower($order_by), [ 'asc', 'desc' ] ) ) { + throw new \RuntimeException('Parameter order_by must be "asc" or "desc"'); + } + if ( !isset($page) || !isset($objects_per_page) ) { - return $this->searchWithoutPagination($search_term); + return $this->searchWithoutPagination($search_term, $sort_by, $order_by); } $url_parameters = [ @@ -419,6 +426,13 @@ public function search( $search_term, $page = null, $objects_per_page = null ) $url_parameters['per_page'] = $objects_per_page; } + if ( isset($sort_by) ) { + $url_parameters['sort_by'] = $sort_by; + } + if ( isset($order_by) ) { + $url_parameters['order_by'] = $order_by; + } + $url = $this->getURL('search'); $response = $this->getClient()->get( $url, @@ -449,10 +463,14 @@ public function search( $search_term, $page = null, $objects_per_page = null ) * This method will be used internally and automatically by search() to automate pagination * to retrieve all available objects, ignoring the server side limit of fetchable objects. * + * @param string $search_term Search term. + * @param string $sort_by Sort by field name, optional. + * @param string $order_by Sort order, optional. + * * @return mixed Returns array of ZammadAPIClient\Resource\... objects * or this object on failure. */ - private function searchWithoutPagination($search_term) + private function searchWithoutPagination($search_term, $sort_by = null, $order_by = null) { $page = 1; $objects_per_page = 100; @@ -460,7 +478,7 @@ private function searchWithoutPagination($search_term) $objects_of_page = []; do { - $objects_of_page = $this->search( $search_term, $page, $objects_per_page ); + $objects_of_page = $this->search( $search_term, $page, $objects_per_page, $sort_by, $order_by ); if ( !is_array($objects_of_page) ) { return $this; } diff --git a/src/Resource/Tag.php b/src/Resource/Tag.php index de1d455..a9d640e 100644 --- a/src/Resource/Tag.php +++ b/src/Resource/Tag.php @@ -13,7 +13,11 @@ class Tag extends AbstractResource 'get' => 'tags', 'search' => 'tag_search?term={query}', 'add' => 'tags/add', - 'remove' => 'tags/remove' + 'remove' => 'tags/remove', + 'all' => 'tag_list', + 'create' => 'tag_list', + 'update' => 'tag_list/{object_id}', + 'delete' => 'tag_list/{object_id}', ]; /** @@ -101,7 +105,7 @@ public function add($object_id, $tag, $object_type = 'Ticket') * @return mixed Returns array of ZammadAPIClient\Resource\... objects * or this object on failure. */ - public function search($search_term, $page = null, $objects_per_page = null) + public function search($search_term, $page = null, $objects_per_page = null, $sort_by = null, $order_by = null) { $this->clearError(); diff --git a/src/Resource/TicketArticle.php b/src/Resource/TicketArticle.php index c36c71a..f14b2f6 100644 --- a/src/Resource/TicketArticle.php +++ b/src/Resource/TicketArticle.php @@ -117,6 +117,9 @@ public function getAttachmentContent($attachment_id) } $content = $response->getBody(); + if ( $content instanceof \Psr\Http\Message\StreamInterface ) { + $content = $content->getContents(); + } return $content; } } diff --git a/test/ZammadAPIClient/ClientTest.php b/test/ZammadAPIClient/ClientTest.php index a38bba6..01db0cb 100644 --- a/test/ZammadAPIClient/ClientTest.php +++ b/test/ZammadAPIClient/ClientTest.php @@ -5,10 +5,15 @@ use PHPUnit\Framework\TestCase; use ZammadAPIClient\Client; +use ZammadAPIClient\EnvConfigTrait; +use ZammadAPIClient\HTTPClientInterface; use GuzzleHttp\Exception\ConnectException; +use Psr\Http\Message\ResponseInterface; class ClientTest extends TestCase { + use EnvConfigTrait; + public function testNetworkError() { // When providing a wrong URL, there must be a proper exception thrown. @@ -21,4 +26,80 @@ public function testNetworkError() ]); $client->get('/nonexisting'); } + + public function testSetsFromHeaderWhenOnBehalfOfUserIsSet() + { + $client = $this->createClientWithMockHttpClient(); + $client->setOnBehalfOfUser('testuser'); + $client->get('tickets'); + + $this->assertArrayHasKey('From', $this->capturedOptions['headers']); + $this->assertSame('testuser', $this->capturedOptions['headers']['From']); + } + + public function testDoesNotSetFromHeaderByDefault() + { + $client = $this->createClientWithMockHttpClient(); + $client->get('tickets'); + + $this->assertArrayNotHasKey('From', $this->capturedOptions['headers']); + } + + public function testRemovesFromHeaderAfterUnsetOnBehalfOfUser() + { + $client = $this->createClientWithMockHttpClient(); + $client->setOnBehalfOfUser('testuser'); + $client->unsetOnBehalfOfUser(); + $client->get('tickets'); + + $this->assertArrayNotHasKey('From', $this->capturedOptions['headers']); + } + + public function testFromHeaderAgainstZammad() + { + $client = self::createZammadClient(); + if (!$client) { + $this->markTestSkipped( + 'Zammad environment variables not set. ' + . 'Set ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL, ' + . 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME, ' + . 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD.' + ); + } + + $client->setOnBehalfOfUser(getenv('ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME')); + $response = $client->get('users'); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertFalse($response->hasError()); + + $client->unsetOnBehalfOfUser(); + $response = $client->get('users'); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertFalse($response->hasError()); + } + + private $capturedOptions = []; + + private function createClientWithMockHttpClient() + { + $this->capturedOptions = []; + + $mockResponse = $this->createMock(ResponseInterface::class); + + $mockHttpClient = $this->createMock(HTTPClientInterface::class); + $mockHttpClient + ->method('request') + ->will($this->returnCallback(function ($method, $uri, $options) use ($mockResponse) { + $this->capturedOptions = $options; + return $mockResponse; + })); + + return new Client([ + 'url' => 'https://example.com/', + 'username' => 'test', + 'password' => 'test', + ], $mockHttpClient); + } } diff --git a/test/ZammadAPIClient/EnvConfigTrait.php b/test/ZammadAPIClient/EnvConfigTrait.php new file mode 100644 index 0000000..08ea826 --- /dev/null +++ b/test/ZammadAPIClient/EnvConfigTrait.php @@ -0,0 +1,39 @@ + 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL', + 'username' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME', + 'password' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD', + ]; + + foreach ($env_keys as $config_key => $env_key) { + $value = getenv($env_key); + if (empty($value)) { + throw new \RuntimeException("Missing environment variable $env_key"); + } + $config[$config_key] = $value; + } + + return $config; + } + + private static function createZammadClient(array $extra_config = []): ?Client + { + try { + $config = self::getZammadConfig($extra_config); + } + catch (\RuntimeException $e) { + return null; + } + + return new Client($config); + } +} diff --git a/test/ZammadAPIClient/GetIDTest.php b/test/ZammadAPIClient/GetIDTest.php new file mode 100644 index 0000000..75db368 --- /dev/null +++ b/test/ZammadAPIClient/GetIDTest.php @@ -0,0 +1,65 @@ + 'http://localhost:3000/', + 'username' => 'test@example.com', + 'password' => 'test', + ]); + } + + public static function getClient() + { + return self::$client; + } + + public function testGetIDBeforeSave() + { + $object = self::getClient()->resource( ResourceType::TICKET ); + + $this->assertNull( + $object->getID(), + 'getID() must return null for unsaved object.' + ); + } + + public function testGetIDReturnsString() + { + $object = self::getClient()->resource( ResourceType::TICKET ); + $object->setValue('id', 123); + + $id = $object->getID(); + + $this->assertIsString( + $id, + 'getID() must return a string.' + ); + + $this->assertSame( + '123', + $id, + 'getID() must cast integer to string.' + ); + } + + public function testGetIDReturnsNullWhenIdNotSet() + { + $object = self::getClient()->resource( ResourceType::TICKET ); + $object->setValue('title', 'test'); + + $this->assertNull( + $object->getID(), + 'getID() must return null when id is not set.' + ); + } +} diff --git a/test/ZammadAPIClient/Resource/AbstractBaseTest.php b/test/ZammadAPIClient/Resource/AbstractBaseTest.php index 7f1d554..690d46f 100644 --- a/test/ZammadAPIClient/Resource/AbstractBaseTest.php +++ b/test/ZammadAPIClient/Resource/AbstractBaseTest.php @@ -5,10 +5,13 @@ use PHPUnit\Framework\TestCase; use ZammadAPIClient\Client; +use ZammadAPIClient\EnvConfigTrait; use ZammadAPIClient\Exception\AlreadyFetchedObjectException; abstract class AbstractBaseTest extends TestCase { + use EnvConfigTrait; + private static $client; protected $resource_type; protected static $created_objects = []; @@ -17,25 +20,13 @@ abstract class AbstractBaseTest extends TestCase public static function setUpBeforeClass(): void { - $client_config = [ - # Set a high timeout for tests to work with slow CI. - 'timeout' => 30, - 'debug' => getenv('ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_DEBUG'), - ]; - - $env_keys = [ - 'url' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL', - 'username' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME', - 'password' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD', - ]; - foreach ( $env_keys as $config_key => $env_key ) { - $value = getenv($env_key); - if ( empty($value) ) { - throw new \RuntimeException("Missing environment variable $env_key"); - } + $client_timeout = getenv('ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_TIMEOUT'); - $client_config[$config_key] = $value; - } + $client_config = self::getZammadConfig([ + 'timeout' => !empty($client_timeout) ? $client_timeout : 30, + 'debug' => getenv('ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_DEBUG'), + 'connection_options' => ['headers' => ['Connection' => 'close']], + ]); self::$client = new Client($client_config); } diff --git a/test/ZammadAPIClient/Resource/TagTest.php b/test/ZammadAPIClient/Resource/TagTest.php index 8b725f6..2fa9dc6 100644 --- a/test/ZammadAPIClient/Resource/TagTest.php +++ b/test/ZammadAPIClient/Resource/TagTest.php @@ -135,6 +135,168 @@ public function testRemove() ); } + public function testAdminAll() + { + $tags = self::getClient()->resource( $this->resource_type )->all(); + + $this->assertIsArray( + $tags, + 'all() must return an array.' + ); + + if ( count($tags) > 0 ) { + $tag = $tags[0]; + $this->assertInstanceOf( + $this->resource_type, + $tag, + 'Elements must be Tag instances.' + ); + $this->assertNotNull( + $tag->getValue('id'), + 'Tag must have an id field.' + ); + $this->assertNotNull( + $tag->getValue('name'), + 'Tag must have a name field.' + ); + } + } + + public function testAdminCreate() + { + $tag_name = self::getUniqueID(); + + $tag = self::getClient()->resource( $this->resource_type ); + $tag->setValue('name', $tag_name); + $saved_tag = $tag->save(); + + $this->assertFalse( + $saved_tag->hasError(), + 'Error must not be set after creating tag.' + ); + + $tags = self::getClient()->resource( $this->resource_type )->all(); + $found = false; + foreach ( $tags as $t ) { + if ( $t->getValue('name') === $tag_name ) { + $found = true; + break; + } + } + + $this->assertTrue( + $found, + 'Created tag must appear in all().' + ); + } + + public function testAdminUpdate() + { + $tag_name = self::getUniqueID(); + + $tag = self::getClient()->resource( $this->resource_type ); + $tag->setValue('name', $tag_name); + $tag->save(); + + $this->assertFalse( + $tag->hasError(), + 'Error must not be set after creating tag for update.' + ); + + $tags = self::getClient()->resource( $this->resource_type )->all(); + $found_tag = null; + foreach ( $tags as $t ) { + if ( $t->getValue('name') === $tag_name ) { + $found_tag = $t; + break; + } + } + + $this->assertNotNull( + $found_tag, + 'Created tag must be found in all().' + ); + + $new_name = $tag_name . '_updated'; + $found_tag->setValue('name', $new_name); + $found_tag->save(); + + $this->assertFalse( + $found_tag->hasError(), + 'Error must not be set after updating tag.' + ); + + $tags = self::getClient()->resource( $this->resource_type )->all(); + $found_updated = false; + $found_old = false; + foreach ( $tags as $t ) { + if ( $t->getValue('name') === $new_name ) { + $found_updated = true; + } + if ( $t->getValue('name') === $tag_name ) { + $found_old = true; + } + } + + $this->assertTrue( + $found_updated, + 'Updated tag name must appear in all().' + ); + $this->assertFalse( + $found_old, + 'Old tag name must not appear in all().' + ); + } + + public function testAdminDelete() + { + $tag_name = self::getUniqueID(); + + $tag = self::getClient()->resource( $this->resource_type ); + $tag->setValue('name', $tag_name); + $tag->save(); + + $this->assertFalse( + $tag->hasError(), + 'Error must not be set after creating tag for delete.' + ); + + $tags = self::getClient()->resource( $this->resource_type )->all(); + $found_tag = null; + foreach ( $tags as $t ) { + if ( $t->getValue('name') === $tag_name ) { + $found_tag = $t; + break; + } + } + + $this->assertNotNull( + $found_tag, + 'Created tag must be found in all() before delete.' + ); + + $found_tag->delete(); + + $this->assertFalse( + $found_tag->hasError(), + 'Error must not be set after deleting tag.' + ); + + $tags = self::getClient()->resource( $this->resource_type )->all(); + $found = false; + foreach ( $tags as $t ) { + if ( $t->getValue('name') === $tag_name ) { + $found = true; + break; + } + } + + $this->assertFalse( + $found, + 'Deleted tag must not appear in all().' + ); + } + private static function createTicket() { self::$ticket = self::getClient()->resource( ResourceType::TICKET ); diff --git a/test/ZammadAPIClient/Resource/TicketArticleTest.php b/test/ZammadAPIClient/Resource/TicketArticleTest.php index f42b0f9..7eb4e3f 100644 --- a/test/ZammadAPIClient/Resource/TicketArticleTest.php +++ b/test/ZammadAPIClient/Resource/TicketArticleTest.php @@ -166,7 +166,7 @@ public function testCreate( $values, $expected_success ) // Fetch attachment content $content = $object->getAttachmentContent( $attachment['id'] ); - $this->assertEquals( + $this->assertSame( $expected_content, $content, "Content of file $filename must match expected one."