From 58430e4537e1091af8ad2281d2f8848c9a6288d7 Mon Sep 17 00:00:00 2001 From: Sean Johnson Date: Thu, 27 Oct 2016 01:34:27 -0500 Subject: [PATCH] Domains API, #187 (#198) --- .gitignore | 1 + .travis.yml | 7 +- composer.json | 2 +- phpunit.xml.dist | 19 + src/Mailgun/Api/AbstractApi.php | 110 +++++- src/Mailgun/Api/Domain.php | 269 +++++++++++++ src/Mailgun/Mailgun.php | 8 + .../Resource/Api/Domain/ComplexDomain.php | 95 +++++ .../Resource/Api/Domain/Credential.php | 113 ++++++ .../Api/Domain/CredentialListResponse.php | 78 ++++ .../Api/Domain/DeliverySettingsResponse.php | 74 ++++ .../Domain/DeliverySettingsUpdateResponse.php | 91 +++++ .../Resource/Api/Domain/DomainDnsRecord.php | 141 +++++++ .../Api/Domain/DomainListResponse.php | 95 +++++ .../Resource/Api/Domain/SimpleDomain.php | 167 +++++++++ src/Mailgun/Resource/Api/SimpleResponse.php | 99 +++++ tests/Api/StatsTest.php | 34 +- tests/Api/TestCase.php | 42 ++- tests/Integration/DomainApiTest.php | 352 ++++++++++++++++++ 19 files changed, 1775 insertions(+), 22 deletions(-) create mode 100644 src/Mailgun/Api/Domain.php create mode 100644 src/Mailgun/Resource/Api/Domain/ComplexDomain.php create mode 100644 src/Mailgun/Resource/Api/Domain/Credential.php create mode 100644 src/Mailgun/Resource/Api/Domain/CredentialListResponse.php create mode 100644 src/Mailgun/Resource/Api/Domain/DeliverySettingsResponse.php create mode 100644 src/Mailgun/Resource/Api/Domain/DeliverySettingsUpdateResponse.php create mode 100644 src/Mailgun/Resource/Api/Domain/DomainDnsRecord.php create mode 100644 src/Mailgun/Resource/Api/Domain/DomainListResponse.php create mode 100644 src/Mailgun/Resource/Api/Domain/SimpleDomain.php create mode 100644 src/Mailgun/Resource/Api/SimpleResponse.php create mode 100644 tests/Integration/DomainApiTest.php diff --git a/.gitignore b/.gitignore index dddbbe4..1785fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ composer.lock nbproject/* .idea phpunit.phar +modd.conf diff --git a/.travis.yml b/.travis.yml index fadbf8c..b50b894 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: php - + php: - 5.5 - 5.6 @@ -11,6 +11,9 @@ before_install: install: - travis_retry composer install + - travis_retry composer require --dev 'phpunit/php-code-coverage=^2.2.4' script: - - phpunit \ No newline at end of file + - phpunit -c phpunit.xml.dist --testsuite unit --coverage-text + - phpunit -c phpunit.xml.dist --testsuite functional --coverage-text + - '[[ "${TRAVIS_PULL_REQUEST}" == "false" ]] && ( phpunit -c phpunit.xml.dist --testsuite integration --coverage-text ) || ( echo "Testing PR - No integration tests available")' diff --git a/composer.json b/composer.json index 0381a24..c429c5d 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "webmozart/assert": "^1.1" }, "require-dev": { - "phpunit/phpunit": "~4.6", + "phpunit/phpunit": "~4.8", "php-http/guzzle6-adapter": "^1.0" }, "autoload": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8707107..bbe28c2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,7 +13,26 @@ tests + + + tests + tests/Integration + test/Functional + + + + tests/Integration + + + + tests/Functional + + + + src + + diff --git a/src/Mailgun/Api/AbstractApi.php b/src/Mailgun/Api/AbstractApi.php index ec4b08e..88ceed4 100644 --- a/src/Mailgun/Api/AbstractApi.php +++ b/src/Mailgun/Api/AbstractApi.php @@ -4,10 +4,15 @@ namespace Mailgun\Api; use Http\Client\Common\HttpMethodsClient; use Http\Client\HttpClient; -use Http\Message\RequestFactory; -use Mailgun\Exception\HttpServerException; use Http\Client\Exception as HttplugException; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Discovery\StreamFactoryDiscovery; +use Http\Message\RequestFactory; +use Http\Message\MultipartStream\MultipartStreamBuilder; +use Mailgun\Assert; +use Mailgun\Exception\HttpServerException; use Mailgun\Serializer\ResponseDeserializer; +use Mailgun\Resource\Api\SimpleResponse; use Psr\Http\Message\ResponseInterface; /** @@ -38,6 +43,25 @@ abstract class AbstractApi $this->serializer = $serializer; } + /** + * Attempts to safely deserialize the response into the given class. + * If the HTTP return code != 200, deserializes into SimpleResponse::class + * to contain the error message and any other information provided. + * + * @param ResponseInterface $response + * @param string $className + * + * @return $class|SimpleResponse + */ + protected function safeDeserialize(ResponseInterface $response, $className) + { + if ($response->getStatusCode() !== 200) { + return $this->serializer->deserialize($response, SimpleResponse::class); + } else { + return $this->serializer->deserialize($response, $className); + } + } + /** * Send a GET request with query parameters. * @@ -76,6 +100,20 @@ abstract class AbstractApi return $this->postRaw($path, $this->createJsonBody($parameters), $requestHeaders); } + /** + * Send a POST request with parameters encoded as multipart-stream form data. + * + * @param string $path Request path. + * @param array $parameters POST parameters to be mutipart-stream-encoded. + * @param array $requestHeaders Request headers. + * + * @return ResponseInterface + */ + protected function postMultipart($path, array $parameters = [], array $requestHeaders = []) + { + return $this->doMultipart('POST', $path, $parameters, $requestHeaders); + } + /** * Send a POST request with raw data. * @@ -116,6 +154,20 @@ abstract class AbstractApi return $response; } + /** + * Send a PUT request with parameters encoded as multipart-stream form data. + * + * @param string $path Request path. + * @param array $parameters PUT parameters to be mutipart-stream-encoded. + * @param array $requestHeaders Request headers. + * + * @return ResponseInterface + */ + protected function putMultipart($path, array $parameters = [], array $requestHeaders = []) + { + return $this->doMultipart('PUT', $path, $parameters, $requestHeaders); + } + /** * Send a DELETE request with JSON-encoded parameters. * @@ -136,6 +188,60 @@ abstract class AbstractApi return $response; } + /** + * Send a DELETE request with parameters encoded as multipart-stream form data. + * + * @param string $path Request path. + * @param array $parameters DELETE parameters to be mutipart-stream-encoded. + * @param array $requestHeaders Request headers. + * + * @return ResponseInterface + */ + protected function deleteMultipart($path, array $parameters = [], array $requestHeaders = []) + { + return $this->doMultipart('DELETE', $path, $parameters, $requestHeaders); + } + + /** + * Send a request with parameters encoded as multipart-stream form data. + * + * @param string $type Request type. (POST, PUT, etc.) + * @param string $path Request path. + * @param array $parameters POST parameters to be mutipart-stream-encoded. + * @param array $requestHeaders Request headers. + * + * @return ResponseInterface + */ + protected function doMultipart($type, $path, array $parameters = [], array $requestHeaders = []) + { + Assert::oneOf( + $type, + [ + 'DELETE', + 'POST', + 'PUT', + ] + ); + + $streamFactory = StreamFactoryDiscovery::find(); + $builder = new MultipartStreamBuilder($streamFactory); + foreach ($parameters as $k => $v) { + $builder->addResource($k, $v); + } + + $multipartStream = $builder->build(); + $boundary = $builder->getBoundary(); + + $request = MessageFactoryDiscovery::find()->createRequest( + $type, + $path, + ['Content-Type' => 'multipart/form-data; boundary='.$boundary], + $multipartStream + ); + + return $this->httpClient->sendRequest($request); + } + /** * Create a JSON encoded version of an array of parameters. * diff --git a/src/Mailgun/Api/Domain.php b/src/Mailgun/Api/Domain.php new file mode 100644 index 0000000..d0244fc --- /dev/null +++ b/src/Mailgun/Api/Domain.php @@ -0,0 +1,269 @@ + + */ +class Domain extends AbstractApi +{ + /** + * Returns a list of domains on the account. + * + * @param int $limit + * @param int $skip + * + * @return DomainListResponse + */ + public function listAll($limit = 100, $skip = 0) + { + Assert::integer($limit); + Assert::integer($skip); + + $params = [ + 'limit' => $limit, + 'skip' => $skip, + ]; + + $response = $this->get('/v3/domains', $params); + + return $this->serializer->deserialize($response, DomainListResponse::class); + } + + /** + * Returns a single domain. + * + * @param string $domain Name of the domain. + * + * @return ComplexDomain|array|ResponseInterface + */ + public function info($domain) + { + Assert::stringNotEmpty($domain); + + $response = $this->get(sprintf('/v3/domains/%s', $domain)); + + return $this->serializer->deserialize($response, ComplexDomain::class); + } + + /** + * Creates a new domain for the account. + * See below for spam filtering parameter information. + * {@link https://documentation.mailgun.com/user_manual.html#um-spam-filter}. + * + * @param string $domain Name of the domain. + * @param string $smtpPass Password for SMTP authentication. + * @param string $spamAction `disable` or `tag` - inbound spam filtering. + * @param bool $wildcard Domain will accept email for subdomains. + * + * @return ComplexDomain|array|ResponseInterface + */ + public function create($domain, $smtpPass, $spamAction, $wildcard) + { + Assert::stringNotEmpty($domain); + Assert::stringNotEmpty($smtpPass); + // TODO(sean.johnson): Extended spam filter input validation. + Assert::stringNotEmpty($spamAction); + Assert::boolean($wildcard); + + $params = [ + 'name' => $domain, + 'smtp_password' => $smtpPass, + 'spam_action' => $spamAction, + 'wildcard' => $wildcard, + ]; + + $response = $this->postMultipart('/v3/domains', $params); + + return $this->safeDeserialize($response, ComplexDomain::class); + } + + /** + * Removes a domain from the account. + * WARNING: This action is irreversible! Be cautious! + * + * @param string $domain Name of the domain. + * + * @return SimpleResponse|array|ResponseInterface + */ + public function remove($domain) + { + Assert::stringNotEmpty($domain); + + $response = $this->delete(sprintf('/v3/domains/%s', $domain)); + + return $this->serializer->deserialize($response, SimpleResponse::class); + } + + /** + * Returns a list of SMTP credentials for the specified domain. + * + * @param string $domain Name of the domain. + * @param int $limit Number of credentials to return + * @param int $skip Number of credentials to omit from the list + * + * @return CredentialsListResponse + */ + public function listCredentials($domain, $limit = 100, $skip = 0) + { + Assert::stringNotEmpty($domain); + Assert::integer($limit); + Assert::integer($skip); + + $params = [ + 'limit' => $limit, + 'skip' => $skip, + ]; + + $response = $this->get(sprintf('/v3/domains/%s/credentials', $domain), $params); + + return $this->safeDeserialize($response, CredentialListResponse::class); + } + + /** + * Create a new SMTP credential pair for the specified domain. + * + * @param string $domain Name of the domain. + * @param string $login SMTP Username. + * @param string $password SMTP Password. Length min 5, max 32. + * + * @return Credential|array|ResponseInterface + */ + public function newCredential($domain, $login, $password) + { + Assert::stringNotEmpty($domain); + Assert::stringNotEmpty($login); + Assert::stringNotEmpty($password); + Assert::lengthBetween($password, 5, 32, 'SMTP password must be between 5 and 32 characters.'); + + $params = [ + 'login' => $login, + 'password' => $password, + ]; + + $response = $this->postMultipart(sprintf('/v3/domains/%s/credentials', $domain), $params); + + return $this->serializer->deserialize($response, SimpleResponse::class); + } + + /** + * Update a set of SMTP credentials for the specified domain. + * + * @param string $domain Name of the domain. + * @param string $login SMTP Username. + * @param string $pass New SMTP Password. Length min 5, max 32. + * + * @return SimpleResponse|array|ResponseInterface + */ + public function updateCredential($domain, $login, $pass) + { + Assert::stringNotEmpty($domain); + Assert::stringNotEmpty($login); + Assert::stringNotEmpty($pass); + Assert::lengthBetween($pass, 5, 32, 'SMTP password must be between 5 and 32 characters.'); + + $params = [ + 'password' => $pass, + ]; + + $response = $this->putMultipart( + sprintf( + '/v3/domains/%s/credentials/%s', + $domain, + $login + ), + $params + ); + + return $this->serializer->deserialize($response, SimpleResponse::class); + } + + /** + * Remove a set of SMTP credentials from the specified domain. + * + * @param string $domain Name of the domain. + * @param string $login SMTP Username. + * + * @return SimpleResponse|array|ResponseInterface + */ + public function deleteCredential($domain, $login) + { + Assert::stringNotEmpty($domain); + Assert::stringNotEmpty($login); + + $response = $this->delete( + sprintf( + '/v3/domains/%s/credentials/%s', + $domain, + $login + ) + ); + + return $this->serializer->deserialize($response, SimpleResponse::class); + } + + /** + * Returns delivery connection settings for the specified domain. + * + * @param string $domain Name of the domain. + * + * @return DeliverySettingsResponse|array|ResponseInterface + */ + public function getDeliverySettings($domain) + { + Assert::stringNotEmpty($domain); + + $response = $this->get(sprintf('/v3/domains/%s/connection', $domain)); + + return $this->serializer->deserialize($response, DeliverySettingsResponse::class); + } + + /** + * Updates the specified delivery connection settings for the specified domain. + * If a parameter is passed in as null, it will not be updated. + * + * @param string $domain Name of the domain. + * @param bool|null $requireTLS Enforces that messages are sent only over a TLS connection. + * @param bool|null $noVerify Disables TLS certificate and hostname verification. + * + * @return DeliverySettingsResponse|array|ResponseInterface + */ + public function updateDeliverySettings($domain, $requireTLS, $noVerify) + { + Assert::stringNotEmpty($domain); + Assert::nullOrBoolean($requireTLS); + Assert::nullOrBoolean($noVerify); + + $params = []; + + if (null !== $requireTLS) { + $params['require_tls'] = $requireTLS ? 'true' : 'false'; + } + + if (null !== $noVerify) { + $params['skip_verification'] = $noVerify ? 'true' : 'false'; + } + + $response = $this->putMultipart(sprintf('/v3/domains/%s/connection', $domain), $params); + + return $this->serializer->deserialize($response, DeliverySettingsUpdateResponse::class); + } +} diff --git a/src/Mailgun/Mailgun.php b/src/Mailgun/Mailgun.php index 2d0071a..69482c5 100644 --- a/src/Mailgun/Mailgun.php +++ b/src/Mailgun/Mailgun.php @@ -260,4 +260,12 @@ class Mailgun { return new Api\Stats($this->httpClient, $this->requestFactory, $this->serializer); } + + /** + * @return Api\Domain + */ + public function getDomainApi() + { + return new Api\Domain($this->httpClient, $this->requestFactory, $this->serializer); + } } diff --git a/src/Mailgun/Resource/Api/Domain/ComplexDomain.php b/src/Mailgun/Resource/Api/Domain/ComplexDomain.php new file mode 100644 index 0000000..cc8cc74 --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/ComplexDomain.php @@ -0,0 +1,95 @@ + + */ +class ComplexDomain implements CreatableFromArray +{ + /** + * @var SimpleDomain + */ + private $domain; + + /** + * @var DomainDnsRecord[] + */ + private $inboundDnsRecords; + + /** + * @var DomainDnsRecord[] + */ + private $outboundDnsRecords; + + /** + * @param array $data + * + * @return ComplexDomain + */ + public static function createFromArray(array $data) + { + Assert::keyExists($data, 'domain'); + Assert::keyExists($data, 'receiving_dns_records'); + Assert::keyExists($data, 'sending_dns_records'); + + // Let DomainDnsRecord::createFromArray() handle validation of + // the `receiving_dns_records` and `sending_dns_records` data. + // Also let SimpleDomain::createFromArray() handle validation of + // the `domain` fields. + return new static( + SimpleDomain::createFromArray($data['domain']), + DomainDnsRecord::createFromArray($data['receiving_dns_records']), + DomainDnsRecord::createFromArray($data['sending_dns_records']) + ); + } + + /** + * @param SimpleDomain $domainInfo + * @param array $rxRecords Array of DomainDnsRecord instances + * @param array $txRecords Array of DomainDnsRecord instances + */ + public function __construct(SimpleDomain $domainInfo, array $rxRecords, array $txRecords) + { + $this->domain = $domainInfo; + $this->inboundDnsRecords = $rxRecords; + $this->outboundDnsRecords = $txRecords; + } + + /** + * @return SimpleDomain + */ + public function getDomain() + { + return $this->domain; + } + + /** + * @return DomainDnsRecord[] + */ + public function getInboundDNSRecords() + { + return $this->inboundDnsRecords; + } + + /** + * @return DomainDnsRecord[] + */ + public function getOutboundDNSRecords() + { + return $this->outboundDnsRecords; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/Credential.php b/src/Mailgun/Resource/Api/Domain/Credential.php new file mode 100644 index 0000000..55bbe07 --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/Credential.php @@ -0,0 +1,113 @@ + + */ +class Credential implements CreatableFromArray +{ + /** + * @var int|null + */ + private $sizeBytes; + + /** + * @var \DateTime + */ + private $createdAt; + + /** + * @var string + */ + private $mailbox; + + /** + * @var string + */ + private $login; + + /** + * @param array $data + * + * @return Credential + */ + public static function createFromArray(array $data) + { + Assert::keyExists($data, 'created_at'); + Assert::keyExists($data, 'mailbox'); + Assert::keyExists($data, 'login'); + + $sizeBytes = array_key_exists('size_bytes', $data) ? $data['size_bytes'] : null; + $createdAt = new \DateTime($data['created_at']); + $mailbox = $data['mailbox']; + $login = $data['login']; + + Assert::nullOrInteger($sizeBytes); + Assert::isInstanceOf($createdAt, '\DateTime'); + Assert::string($mailbox); + Assert::string($login); + + return new static( + $sizeBytes, + $createdAt, + $mailbox, + $login + ); + } + + /** + * @param int $sizeBytes + * @param \DateTime $createdAt + * @param string $mailbox + * @param string $login + */ + public function __construct($sizeBytes, \DateTime $createdAt, $mailbox, $login) + { + $this->sizeBytes = $sizeBytes; + $this->createdAt = $createdAt; + $this->mailbox = $mailbox; + $this->login = $login; + } + + /** + * @return int|null + */ + public function getSizeBytes() + { + return $this->sizeBytes; + } + + /** + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->createdAt; + } + + /** + * @return string + */ + public function getMailbox() + { + return $this->mailbox; + } + + /** + * @return string + */ + public function getLogin() + { + return $this->login; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/CredentialListResponse.php b/src/Mailgun/Resource/Api/Domain/CredentialListResponse.php new file mode 100644 index 0000000..e57cd38 --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/CredentialListResponse.php @@ -0,0 +1,78 @@ + + */ +class CredentialListResponse implements CreatableFromArray +{ + /** + * @var int + */ + private $totalCount; + + /** + * @var Credential[] + */ + private $items; + + /** + * @param array $data + * + * @return CredentialListResponse|array|ResponseInterface + */ + public static function createFromArray(array $data) + { + $items = []; + + Assert::keyExists($data, 'total_count'); + Assert::keyExists($data, 'items'); + + foreach ($data['items'] as $item) { + $items[] = Credential::createFromArray($item); + } + + return new self($data['total_count'], $items); + } + + /** + * @param int $totalCount + * @param Credential[] $items + */ + public function __construct($totalCount, array $items) + { + Assert::integer($totalCount); + Assert::isArray($items); + Assert::allIsInstanceOf($items, 'Mailgun\Resource\Api\Domain\Credential'); + + $this->totalCount = $totalCount; + $this->items = $items; + } + + /** + * @return int + */ + public function getTotalCount() + { + return $this->totalCount; + } + + /** + * @return Credential[] + */ + public function getCredentials() + { + return $this->items; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/DeliverySettingsResponse.php b/src/Mailgun/Resource/Api/Domain/DeliverySettingsResponse.php new file mode 100644 index 0000000..9677073 --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/DeliverySettingsResponse.php @@ -0,0 +1,74 @@ + + */ +class DeliverySettingsResponse implements CreatableFromArray +{ + /** + * @var bool + */ + private $noVerify; + + /** + * @var bool + */ + private $requireTLS; + + /** + * @param array $data + * + * @return DeliverySettingsResponse + */ + public static function createFromArray(array $data) + { + Assert::keyExists($data, 'connection'); + Assert::isArray($data['connection']); + $connSettings = $data['connection']; + + Assert::keyExists($connSettings, 'skip_verification'); + Assert::keyExists($connSettings, 'require_tls'); + + return new static( + $connSettings['skip_verification'], + $connSettings['require_tls'] + ); + } + + /** + * @param bool $noVerify Disable remote TLS certificate verification + * @param bool $requireTLS Requires TLS for all outbound communication + */ + public function __construct($noVerify, $requireTLS) + { + $this->noVerify = $noVerify; + $this->requireTLS = $requireTLS; + } + + /** + * @return bool + */ + public function getSkipVerification() + { + return $this->noVerify; + } + + /** + * @return bool + */ + public function getRequireTLS() + { + return $this->requireTLS; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/DeliverySettingsUpdateResponse.php b/src/Mailgun/Resource/Api/Domain/DeliverySettingsUpdateResponse.php new file mode 100644 index 0000000..c6016fc --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/DeliverySettingsUpdateResponse.php @@ -0,0 +1,91 @@ + + */ +class DeliverySettingsUpdateResponse extends SimpleResponse implements CreatableFromArray +{ + /** + * @var bool + */ + private $noVerify; + + /** + * @var bool + */ + private $requireTLS; + + /** + * @param array $data + * + * @return SettingsUpdateResponse + */ + public static function createFromArray(array $data) + { + Assert::keyExists($data, 'message'); + Assert::keyExists($data, 'skip_verification'); + Assert::keyExists($data, 'require_tls'); + + $message = $data['message']; + $noVerify = $data['skip_verification']; + $requireTLS = $data['require_tls']; + + Assert::nullOrString($message); + Assert::boolean($noVerify); + Assert::boolean($requireTLS); + + return new static( + $message, + $noVerify, + $requireTLS + ); + } + + /** + * @param string $message + * @param bool $noVerify + * @param bool $requireTLS + */ + public function __construct($message, $noVerify, $requireTLS) + { + $this->message = $message; + $this->noVerify = $noVerify; + $this->requireTLS = $requireTLS; + } + + /** + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * @return bool + */ + public function getSkipVerification() + { + return $this->noVerify; + } + + /** + * @return bool + */ + public function getRequireTLS() + { + return $this->requireTLS; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/DomainDnsRecord.php b/src/Mailgun/Resource/Api/Domain/DomainDnsRecord.php new file mode 100644 index 0000000..0a4d1a3 --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/DomainDnsRecord.php @@ -0,0 +1,141 @@ + + */ +class DomainDnsRecord implements CreatableFromArray +{ + /** + * @var string|null + */ + private $name; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $value; + + /** + * @var string|null + */ + private $priority; + + /** + * @var string + */ + private $valid; + + /** + * @param array $data + * + * @return DomainDnsRecord[]|array|ResponseInterface + */ + public static function createFromArray(array $data) + { + $items = []; + + foreach ($data as $item) { + Assert::keyExists($item, 'record_type'); + Assert::keyExists($item, 'value'); + Assert::keyExists($item, 'valid'); + + $items[] = new static( + array_key_exists('name', $item) ? $item['name'] : null, + $item['record_type'], + $item['value'], + array_key_exists('priority', $item) ? $item['priority'] : null, + $item['valid'] + ); + } + + return $items; + } + + /** + * @param string|null $name Name of the record, as used in CNAME, etc. + * @param string $type DNS record type + * @param string $value DNS record value + * @param string|null $priority Record priority, used for MX + * @param string $valid DNS record has been added to domain DNS? + */ + public function __construct($name, $type, $value, $priority, $valid) + { + Assert::nullOrString($name); + Assert::string($type); + Assert::string($value); + Assert::nullOrString($priority); + Assert::string($valid); + + $this->name = $name; + $this->type = $type; + $this->value = $value; + $this->priority = $priority; + $this->valid = $valid; + } + + /** + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @return value + */ + public function getValue() + { + return $this->value; + } + + /** + * @return string|null + */ + public function getPriority() + { + return $this->priority; + } + + /** + * @return bool + */ + public function isValid() + { + return 'valid' === $this->value; + } + + /** + * @return string + */ + public function getValidity() + { + return $this->valid; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/DomainListResponse.php b/src/Mailgun/Resource/Api/Domain/DomainListResponse.php new file mode 100644 index 0000000..fd9a425 --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/DomainListResponse.php @@ -0,0 +1,95 @@ + + */ +class DomainListResponse implements CreatableFromArray +{ + /** + * @var int + */ + private $totalCount; + + /** + * @var SimpleDomain[] + */ + private $items; + + /** + * @param array $data + * + * @return DomainListResponse|array|ResponseInterface + */ + public static function createFromArray(array $data) + { + $items = []; + + Assert::keyExists($data, 'total_count'); + Assert::keyExists($data, 'items'); + + foreach ($data['items'] as $item) { + Assert::keyExists($item, 'name'); + Assert::keyExists($item, 'smtp_login'); + Assert::keyExists($item, 'smtp_password'); + Assert::keyExists($item, 'wildcard'); + Assert::keyExists($item, 'spam_action'); + Assert::keyExists($item, 'state'); + Assert::keyExists($item, 'created_at'); + + $items[] = SimpleDomain::createFromArray($item); + $items[] = new SimpleDomain( + $item['name'], + $item['smtp_login'], + $item['smtp_password'], + $item['wildcard'], + $item['spam_action'], + $item['state'], + new \DateTime($item['created_at']) + ); + } + + return new self($data['total_count'], $items); + } + + /** + * @param int $totalCount + * @param SimpleDomain[] $items + */ + public function __construct($totalCount, array $items) + { + Assert::integer($totalCount); + Assert::isArray($items); + Assert::allIsInstanceOf($items, 'Mailgun\Resource\Api\Domain\SimpleDomain'); + + $this->totalCount = $totalCount; + $this->items = $items; + } + + /** + * @return int + */ + public function getTotalCount() + { + return $this->totalCount; + } + + /** + * @return SimpleDomain[] + */ + public function getDomains() + { + return $this->items; + } +} diff --git a/src/Mailgun/Resource/Api/Domain/SimpleDomain.php b/src/Mailgun/Resource/Api/Domain/SimpleDomain.php new file mode 100644 index 0000000..f78f8bc --- /dev/null +++ b/src/Mailgun/Resource/Api/Domain/SimpleDomain.php @@ -0,0 +1,167 @@ + + */ +class SimpleDomain implements CreatableFromArray +{ + /** + * @var \DateTime + */ + private $createdAt; + + /** + * @var string + */ + private $smtpLogin; + + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $smtpPassword; + + /** + * @var bool + */ + private $wildcard; + + /** + * @var string + */ + private $spamAction; + + /** + * @var string + */ + private $state; + + /** + * @param array $data + * + * @return SimpleDomain + */ + public static function createFromArray(array $data) + { + Assert::isArray($data); + + Assert::keyExists($data, 'name'); + Assert::keyExists($data, 'smtp_login'); + Assert::keyExists($data, 'smtp_password'); + Assert::keyExists($data, 'wildcard'); + Assert::keyExists($data, 'spam_action'); + Assert::keyExists($data, 'state'); + Assert::keyExists($data, 'created_at'); + + return new static( + $data['name'], + $data['smtp_login'], + $data['smtp_password'], + $data['wildcard'], + $data['spam_action'], + $data['state'], + new \DateTime($data['created_at']) + ); + } + + /** + * @param string $name + * @param string $smtpLogin + * @param string $smtpPass + * @param bool $wildcard + * @param string $spamAction + * @param string $state + * @param \DateTime $createdAt + */ + public function __construct($name, $smtpLogin, $smtpPassword, $wildcard, $spamAction, $state, \DateTime $createdAt) + { + Assert::string($name); + Assert::string($smtpLogin); + Assert::string($smtpPassword); + Assert::boolean($wildcard); + Assert::string($spamAction); + Assert::string($state); + Assert::isInstanceOf($createdAt, '\DateTime'); + + $this->name = $name; + $this->smtpLogin = $smtpLogin; + $this->smtpPassword = $smtpPassword; + $this->wildcard = $wildcard; + $this->spamAction = $spamAction; + $this->state = $state; + $this->createdAt = $createdAt; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getSmtpUsername() + { + return $this->smtpLogin; + } + + /** + * @return string + */ + public function getSmtpPassword() + { + return $this->smtpPassword; + } + + /** + * @return bool + */ + public function isWildcard() + { + return $this->wildcard; + } + + /** + * @return string + */ + public function getSpamAction() + { + return $this->spamAction; + } + + /** + * @return string + */ + public function getState() + { + return $this->state; + } + + /** + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->createdAt; + } +} diff --git a/src/Mailgun/Resource/Api/SimpleResponse.php b/src/Mailgun/Resource/Api/SimpleResponse.php new file mode 100644 index 0000000..bb11999 --- /dev/null +++ b/src/Mailgun/Resource/Api/SimpleResponse.php @@ -0,0 +1,99 @@ + + */ +class SimpleResponse implements CreatableFromArray +{ + /** + * @var string + */ + private $message; + + /** + * Only set when API rate limit is hit and a rate limit response is returned. + * + * @var int + */ + private $retrySeconds = null; + + /** + * Only set on calls such as DELETE /v3/domains/.../credentials/. + * + * @var string + */ + private $spec = null; + + /** + * @param array $data + * + * @return SimpleResponse + */ + public static function createFromArray(array $data) + { + $message = array_key_exists('message', $data) ? $data['message'] : null; + $retrySeconds = array_key_exists('retry_seconds', $data) ? $data['retry_seconds'] : null; + $spec = array_key_exists('spec', $data) ? $data['spec'] : null; + + return new static($message, $retrySeconds, $spec); + } + + /** + * @param string|null $message + * @param int|null $retrySeconds + * @param string|null $spec + */ + public function __construct($message, $retrySeconds, $spec) + { + Assert::nullOrString($message); + Assert::nullOrInteger($retrySeconds); + Assert::nullOrString($spec); + + $this->message = $message; + $this->retrySeconds = $retrySeconds; + $this->spec = $spec; + } + + /** + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * @return string + */ + public function getSpec() + { + return $this->spec; + } + + /** + * @return bool + */ + public function isRateLimited() + { + return null !== $this->retrySeconds; + } + + /** + * @return int + */ + public function getRetrySeconds() + { + return $this->retrySeconds; + } +} diff --git a/tests/Api/StatsTest.php b/tests/Api/StatsTest.php index 656519c..b17eecc 100644 --- a/tests/Api/StatsTest.php +++ b/tests/Api/StatsTest.php @@ -29,14 +29,15 @@ class StatsTest extends TestCase $api->total('domain', $data); } - /** - * expectedException \Mailgun\Exception\InvalidArgumentException. - */ - //public function testTotalInvalidArgument() - //{ - // $api = $this->getApiMock(); - // $api->total(''); - //} + // /** + // * @expectedException \Mailgun\Exception\InvalidArgumentException + // */ + // public function testTotalInvalidArgument() + // { + // $api = $this->getApiMock(); + // + // $api->total(''); + // } public function testAll() { @@ -53,12 +54,13 @@ class StatsTest extends TestCase $api->all('domain', $data); } - /* - * expectedException \Mailgun\Exception\InvalidArgumentException - */ - //public function testAllInvalidArgument() - //{ - // $api = $this->getApiMock(); - // $api->all(''); - //} + // /** + // * @expectedException \Mailgun\Exception\InvalidArgumentException + // */ + // public function testAllInvalidArgument() + // { + // $api = $this->getApiMock(); + // + // $api->all(''); + // } } diff --git a/tests/Api/TestCase.php b/tests/Api/TestCase.php index 0ac2e93..33eaea1 100644 --- a/tests/Api/TestCase.php +++ b/tests/Api/TestCase.php @@ -8,6 +8,34 @@ namespace Mailgun\Tests\Api; */ abstract class TestCase extends \PHPUnit_Framework_TestCase { + /** + * Private Mailgun API key. + * + * @var string + */ + protected $apiPrivKey; + + /** + * Public Mailgun API key. + * + * @var string + */ + protected $apiPubKey; + + /** + * Domain used for API testing. + * + * @var string + */ + protected $testDomain; + + public function __construct() + { + $this->apiPrivKey = getenv('MAILGUN_PRIV_KEY'); + $this->apiPubKey = getenv('MAILGUN_PUB_KEY'); + $this->testDomain = getenv('MAILGUN_DOMAIN'); + } + abstract protected function getApiClass(); protected function getApiMock() @@ -28,8 +56,20 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase ->getMock(); return $this->getMockBuilder($this->getApiClass()) - ->setMethods(['get', 'post', 'postRaw', 'delete', 'put']) + ->setMethods( + [ + 'get', + 'post', 'postRaw', 'postMultipart', + 'delete', 'deleteMultipart', + 'put', 'putMultipart', + ] + ) ->setConstructorArgs([$httpClient, $requestClient, $serializer]) ->getMock(); } + + protected function getMailgunClient() + { + return new \Mailgun\Mailgun($this->apiPrivKey); + } } diff --git a/tests/Integration/DomainApiTest.php b/tests/Integration/DomainApiTest.php new file mode 100644 index 0000000..239e7f6 --- /dev/null +++ b/tests/Integration/DomainApiTest.php @@ -0,0 +1,352 @@ + + */ +class DomainApiTest extends TestCase +{ + protected function getApiClass() + { + return 'Mailgun\Api\Domains'; + } + + /** + * Performs `GET /v3/domains` and ensures $this->testDomain exists + * in the returned list. + */ + public function testDomainsList() + { + $mg = $this->getMailgunClient(); + + $domainList = $mg->getDomainApi()->listAll(); + $found = false; + foreach ($domainList->getDomains() as $domain) { + if ($domain->getName() === $this->testDomain) { + $found = true; + } + } + + $this->assertContainsOnlyInstancesOf(SimpleDomain::class, $domainList->getDomains()); + $this->assertTrue($found); + } + + /** + * Performs `GET /v3/domains/` and ensures $this->testDomain + * is properly returned. + */ + public function testDomainGet() + { + $mg = $this->getMailgunClient(); + + $domain = $mg->getDomainApi()->info($this->testDomain); + $this->assertNotNull($domain); + $this->assertNotNull($domain->getDomain()); + $this->assertNotNull($domain->getInboundDNSRecords()); + $this->assertNotNull($domain->getOutboundDNSRecords()); + $this->assertEquals($domain->getDomain()->getState(), 'active'); + } + + /** + * Performs `DELETE /v3/domains/` on a non-existent domain. + */ + public function testRemoveDomain_NoExist() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->remove('example.notareal.tld'); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Domain not found', $ret->getMessage()); + } + + /** + * Performs `POST /v3/domains` to attempt to create a domain with valid + * values. + */ + public function testDomainCreate() + { + $mg = $this->getMailgunClient(); + + $domain = $mg->getDomainApi()->create( + 'example.notareal.tld', // domain name + 'exampleOrgSmtpPassword12', // smtp password + 'tag', // default spam action + false // wildcard domain? + ); + $this->assertNotNull($domain); + $this->assertNotNull($domain->getDomain()); + $this->assertNotNull($domain->getInboundDNSRecords()); + $this->assertNotNull($domain->getOutboundDNSRecords()); + } + + /** + * Performs `POST /v3/domains` to attempt to create a domain with duplicate + * values. + */ + public function testDomainCreate_DuplicateValues() + { + $mg = $this->getMailgunClient(); + + $domain = $mg->getDomainApi()->create( + 'example.notareal.tld', // domain name + 'exampleOrgSmtpPassword12', // smtp password + 'tag', // default spam action + false // wildcard domain? + ); + $this->assertNotNull($domain); + $this->assertInstanceOf(SimpleResponse::class, $domain); + $this->assertEquals('This domain name is already taken', $domain->getMessage()); + } + + /** + * Performs `DELETE /v3/domains/` to remove a domain from the account. + */ + public function testRemoveDomain() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->remove('example.notareal.tld'); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Domain has been deleted', $ret->getMessage()); + } + + /** + * Performs `POST /v3/domains//credentials` to add a credential pair + * to the domain. + */ + public function testCreateCredential() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->newCredential( + $this->testDomain, + 'user-test@'.$this->testDomain, + 'Password.01!' + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Created 1 credentials pair(s)', $ret->getMessage()); + } + + /** + * Performs `POST /v3/domains//credentials` to attempt to add an invalid + * credential pair. + * + * @expectedException InvalidArgumentException + */ + public function testCreateCredentialBadPasswordLong() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->newCredential( + $this->testDomain, + 'user-test', + 'ExtremelyLongPasswordThatCertainlyWillNotBeAccepted' + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + } + + /** + * Performs `POST /v3/domains//credentials` to attempt to add an invalid + * credential pair. + * + * @expectedException InvalidArgumentException + */ + public function testCreateCredentialBadPasswordShort() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->newCredential( + $this->testDomain, + 'user-test', + 'no' + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + } + + /** + * Performs `GET /v3/domains//credentials` to get a list of active credentials. + */ + public function testListCredentials() + { + $mg = $this->getMailgunClient(); + + $found = false; + + $ret = $mg->getDomainApi()->listCredentials($this->testDomain); + $this->assertNotNull($ret); + $this->assertInstanceOf(CredentialListResponse::class, $ret); + $this->assertContainsOnlyInstancesOf(Credential::class, $ret->getCredentials()); + + foreach ($ret->getCredentials() as $cred) { + if ($cred->getLogin() === 'user-test@'.$this->testDomain) { + $found = true; + } + } + + $this->assertTrue($found); + } + + /** + * Performs `GET /v3/domains//credentials` on a non-existent domain. + */ + public function testListCredentialsBadDomain() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->listCredentials('mailgun.org'); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Domain not found: mailgun.org', $ret->getMessage()); + } + + /** + * Performs `PUT /v3/domains//credentials/` to update a credential's + * password. + */ + public function testUpdateCredential() + { + $login = 'user-test@'.$this->testDomain; + + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->updateCredential( + $this->testDomain, + $login, + 'Password..02!' + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Password changed', $ret->getMessage()); + } + + /** + * Performs `PUT /v3/domains//credentials/` with a bad password. + * + * @expectedException InvalidArgumentException + */ + public function testUpdateCredentialBadPasswordLong() + { + $login = 'user-test@'.$this->testDomain; + + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->updateCredential( + $this->testDomain, + $login, + 'ThisIsAnExtremelyLongPasswordThatSurelyWontBeAccepted' + ); + $this->assertNotNull($ret); + } + + /** + * Performs `PUT /v3/domains//credentials/` with a bad password. + * + * @expectedException InvalidArgumentException + */ + public function testUpdateCredentialBadPasswordShort() + { + $login = 'user-test@'.$this->testDomain; + + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->updateCredential( + $this->testDomain, + $login, + 'no' + ); + $this->assertNotNull($ret); + } + + /** + * Performs `DELETE /v3/domains//credentials/` to remove a credential + * pair from a domain. + */ + public function testRemoveCredential() + { + $login = 'user-test@'.$this->testDomain; + + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->deleteCredential( + $this->testDomain, + $login + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Credentials have been deleted', $ret->getMessage()); + $this->assertEquals($login, $ret->getSpec()); + } + + /** + * Performs `DELETE /v3/domains//credentials/` to remove an invalid + * credential pair from a domain. + */ + public function testRemoveCredentialNoExist() + { + $login = 'user-noexist-test@'.$this->testDomain; + + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->deleteCredential( + $this->testDomain, + $login + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(SimpleResponse::class, $ret); + $this->assertEquals('Credentials not found', $ret->getMessage()); + } + + /** + * Performs `GET /v3/domains//connection` to retrieve connection settings. + */ + public function testGetDeliverySettings() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->getDeliverySettings($this->testDomain); + $this->assertNotNull($ret); + $this->assertInstanceOf(DeliverySettingsResponse::class, $ret); + $this->assertTrue(is_bool($ret->getSkipVerification())); + $this->assertTrue(is_bool($ret->getRequireTLS())); + } + + /** + * Performs `PUT /v3/domains//connection` to set connection settings. + */ + public function testSetDeliverySettings() + { + $mg = $this->getMailgunClient(); + + $ret = $mg->getDomainApi()->updateDeliverySettings( + $this->testDomain, + true, + false + ); + $this->assertNotNull($ret); + $this->assertInstanceOf(DeliverySettingsUpdateResponse::class, $ret); + $this->assertEquals('Domain connection settings have been updated, may take 10 minutes to fully propagate', $ret->getMessage()); + $this->assertEquals(true, $ret->getRequireTLS()); + $this->assertEquals(false, $ret->getSkipVerification()); + } +}