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."