├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── phpcs.xml.dist
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src
├── Api
│ ├── Channel.php
│ ├── Model
│ │ ├── ChannelInformation.php
│ │ ├── JsonMessage.php
│ │ ├── Message.php
│ │ ├── PlainTextMessage.php
│ │ ├── StatusInformation.php
│ │ └── XmlMessage.php
│ └── Status.php
├── Exception
│ ├── AuthenticationRequiredException.php
│ ├── InvalidUrlException.php
│ └── NchanException.php
├── Http
│ ├── Client.php
│ ├── Request.php
│ ├── Response.php
│ ├── ThrowExceptionIfRequestRequiresAuthenticationClient.php
│ └── Url.php
├── HttpAdapter
│ ├── BasicAuthenticationCredentials.php
│ ├── BearerAuthenticationCredentials.php
│ ├── Credentials.php
│ ├── HttpStreamWrapperClient.php
│ ├── HttpStreamWrapperResponse.php
│ ├── Psr18ClientAdapter.php
│ ├── Psr7ResponseAdapter.php
│ └── WithoutAuthenticationCredentials.php
└── Nchan.php
└── tests
├── Integration
└── HttpAdapter
│ ├── ClientTestCase.php
│ ├── HttpStreamWrapperClientTest.php
│ └── Psr18ClientAdapterTest.php
├── TestServer
├── PhpUnitStartServerListener.php
└── index.php
└── Unit
├── Api
├── ChannelTest.php
├── Model
│ ├── ChannelInformationTest.php
│ ├── JsonMessageTest.php
│ ├── PlainTextMessageTest.php
│ ├── StatusInformationTest.php
│ └── XmlMessageTest.php
└── StatusTest.php
├── Http
├── RequestTest.php
├── ThrowExceptionIfRequestRequiresAuthenticationClientTest.php
└── UrlTest.php
├── HttpAdapter
├── BasicAuthenticationCredentialsTest.php
├── BearerAuthenticationCredentialsTest.php
├── HttpStreamWrapperResponseTest.php
└── WithoutAuthenticationCredentialsTest.php
└── NchanTest.php
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: master
6 | pull_request:
7 |
8 | jobs:
9 | pipeline:
10 | strategy:
11 | matrix:
12 | operating-system: [ubuntu-latest]
13 | php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
14 | name: ${{ matrix.php-version }}
15 | runs-on: ${{ matrix.operating-system }}
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v2
19 |
20 | - name: Setup PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: ${{ matrix.php-version }}
24 |
25 | - name: Prepare
26 | run: composer install
27 |
28 | - name: Testsuite
29 | run: |
30 | vendor/bin/phpunit
31 | vendor/bin/phpcs
32 | vendor/bin/phpstan analyse
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /composer.lock
3 | /phpcs.xml
4 | /phpstan.neon
5 | /phpunit.xml
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Markus Reinhold
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # php-nchan-client
2 |
3 | 
4 |
5 | __Table of contents__
6 |
7 | * [Overview](#overview)
8 | * [Installation and requirements](#installation-and-requirements)
9 | * [Usage](#usage)
10 | * [Publish a message](#publish-a-message)
11 | * [Get channel information](#get-channel-information)
12 | * [Delete a channel](#delete-a-channel)
13 | * [Nchan status information](#nchan-status-information)
14 | * [Authorize requests](#authorize-requests)
15 | * [PSR-18 compatibility](#psr-18-compatibility)
16 |
17 | ## Overview
18 |
19 | This is a PHP client for [https://nchan.io](https://nchan.io).
20 |
21 | ## Installation and requirements
22 |
23 | ```
24 | composer require marein/php-nchan-client
25 | ```
26 |
27 | If you want to use the
28 | [PSR-18 adapter](#psr-18-compatibility),
29 | install a library that implements PSR-18 http client
30 | ([see here](https://packagist.org/providers/psr/http-client-implementation))
31 | and a library that implements PSR-17 http factories
32 | ([see here](https://packagist.org/providers/psr/http-factory-implementation)).
33 |
34 | If you want to use the built-in http client (default if you don't set anything),
35 | enable the php configuration
36 | [allow_url_fopen](http://php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen).
37 |
38 | ## Usage
39 |
40 | The following code examples use the built-in http client.
41 |
42 | ### Publish a message
43 |
44 |
45 | Show code
46 |
47 | ```php
48 | channel('/path-to-publisher-endpoint');
59 | $channelInformation = $channel->publish(
60 | new PlainTextMessage(
61 | 'my-message-name',
62 | 'my message content'
63 | )
64 | );
65 |
66 | // Nchan returns some channel information after publishing a message.
67 | var_dump($channelInformation);
68 | }
69 | ```
70 |
71 |
72 | ### Get channel information
73 |
74 |
75 | Show code
76 |
77 | ```php
78 | channel('/path-to-publisher-endpoint');
88 | $channelInformation = $channel->information();
89 |
90 | var_dump($channelInformation);
91 | }
92 | ```
93 |
94 |
95 | ### Delete a channel
96 |
97 |
98 | Show code
99 |
100 | ```php
101 | channel('/path-to-publisher-endpoint');
111 | $channel->delete();
112 | }
113 | ```
114 |
115 |
116 | ### Nchan status information
117 |
118 | Endpoints with the `nchan_stub_status` directive can be queried as follows.
119 |
120 |
121 | Show code
122 |
123 | ```php
124 | status('/path-to-status-location');
134 | $statusInformation = $status->information();
135 |
136 | var_dump($statusInformation);
137 | }
138 | ```
139 |
140 |
141 | ### Authorize requests
142 |
143 | Endpoints with the `nchan_authorize_request` directive must be authorized.
144 | The constructor of the
145 | [built-in http client](/src/HttpAdapter/HttpStreamWrapperClient.php)
146 | takes an implementation of type
147 | [Credentials](/src/HttpAdapter/Credentials.php).
148 | This library comes with 2 built-in implementations,
149 | [BasicAuthenticationCredentials](/src/HttpAdapter/BasicAuthenticationCredentials.php)
150 | and
151 | [BearerAuthenticationCredentials](/src/HttpAdapter/BearerAuthenticationCredentials.php).
152 |
153 |
154 | Show code
155 |
156 | ```php
157 |
182 |
183 | If you use another http client through the
184 | [PSR-18 adapter](#psr-18-compatibility),
185 | the respective http client has its own extension points to modify the request before it is sent.
186 |
187 | ## PSR-18 compatibility
188 |
189 | This library comes with a PSR-18 compatible
190 | [adapter](/src/HttpAdapter/Psr18ClientAdapter.php).
191 | There are good reasons not to use the built-in client.
192 | It's based on the http stream wrapper and `file_get_contents`.
193 | This closes the TCP connection after each request.
194 | Other clients, see below, can keep the connection open.
195 |
196 | The following example uses
197 | [guzzlehttp/guzzle](https://packagist.org/packages/guzzlehttp/guzzle)
198 | and
199 | [guzzlehttp/psr7](https://packagist.org/packages/guzzlehttp/psr7).
200 |
201 |
202 | Show code
203 |
204 | ```php
205 |
227 |
228 | The following code example uses
229 | [symfony/http-client](https://packagist.org/packages/symfony/http-client)
230 | and
231 | [nyholm/psr7](https://packagist.org/packages/nyholm/psr7).
232 |
233 |
234 | Show code
235 |
236 | ```php
237 |
267 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marein/php-nchan-client",
3 | "description": "A PHP https://nchan.io client.",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Markus Reinhold",
8 | "email": "markusreinhold@icloud.com"
9 | }
10 | ],
11 | "autoload": {
12 | "psr-4": {
13 | "Marein\\Nchan\\": "src/"
14 | }
15 | },
16 | "autoload-dev": {
17 | "psr-4": {
18 | "Marein\\Nchan\\Tests\\": "tests"
19 | }
20 | },
21 | "require": {
22 | "php": "^7.4 || ^8.0",
23 | "ext-json": "*",
24 | "psr/http-client": "^1.0",
25 | "psr/http-message": "^1.0 || ^2.0",
26 | "psr/http-factory": "^1.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^9.5",
30 | "symfony/process": "^5.4",
31 | "squizlabs/php_codesniffer": "3.6.2",
32 | "phpstan/phpstan": "1.3.3",
33 | "symfony/http-client": "^5.4",
34 | "nyholm/psr7": "^1.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 | src/
8 | tests/
9 |
10 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | parameters:
2 | paths:
3 | - src
4 | level: max
5 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 | ./tests/Unit
18 |
19 |
20 | ./tests/Integration
21 |
22 |
23 |
24 |
25 |
26 | ./src
27 |
28 |
29 |
30 |
31 |
32 |
33 | Marein\Nchan\Tests\Integration\HttpAdapter\HttpStreamWrapperClientTest
34 | 127.0.0.1:8000
35 | tests/TestServer/index.php
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/Api/Channel.php:
--------------------------------------------------------------------------------
1 | channelUrl = $channelUrl;
26 | $this->client = new ThrowExceptionIfRequestRequiresAuthenticationClient(
27 | $client
28 | );
29 | }
30 |
31 | /**
32 | * @throws AuthenticationRequiredException
33 | * @throws NchanException
34 | */
35 | public function publish(Message $message): ChannelInformation
36 | {
37 | $response = $this->client->post(
38 | new Request(
39 | $this->channelUrl,
40 | [
41 | 'Accept' => 'application/json',
42 | 'Content-Type' => $message->contentType(),
43 | 'X-EventSource-Event' => $message->name()
44 | ],
45 | $message->content()
46 | )
47 | );
48 |
49 | if (!in_array($response->statusCode(), [Response::CREATED, Response::ACCEPTED])) {
50 | throw new NchanException(
51 | sprintf(
52 | 'Unable to publish to channel. Maybe the channel does not exists. HTTP status code was %s.',
53 | $response->statusCode()
54 | )
55 | );
56 | }
57 |
58 | return ChannelInformation::fromJson($response->body());
59 | }
60 |
61 | /**
62 | * @throws AuthenticationRequiredException
63 | * @throws NchanException
64 | */
65 | public function information(): ChannelInformation
66 | {
67 | $response = $this->client->get(
68 | new Request(
69 | $this->channelUrl,
70 | [
71 | 'Accept' => 'application/json'
72 | ]
73 | )
74 | );
75 |
76 | if ($response->statusCode() !== Response::OK) {
77 | throw new NchanException(
78 | sprintf(
79 | 'Unable to get channel information. Maybe the channel does not exists. HTTP status code was %s.',
80 | $response->statusCode()
81 | )
82 | );
83 | }
84 |
85 | return ChannelInformation::fromJson($response->body());
86 | }
87 |
88 | /**
89 | * @throws AuthenticationRequiredException
90 | * @throws NchanException
91 | */
92 | public function delete(): void
93 | {
94 | $response = $this->client->delete(
95 | new Request(
96 | $this->channelUrl,
97 | [
98 | 'Accept' => 'application/json'
99 | ]
100 | )
101 | );
102 |
103 | if (!in_array($response->statusCode(), [Response::OK, RESPONSE::NOT_FOUND])) {
104 | throw new NchanException(
105 | sprintf(
106 | 'Unable to delete channel. Maybe the channel does not exists. HTTP status code was %s.',
107 | $response->statusCode()
108 | )
109 | );
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/Api/Model/ChannelInformation.php:
--------------------------------------------------------------------------------
1 | numberOfMessages = $numberOfMessages;
45 | $this->secondsSinceLastPublishedMessage = $secondsSinceLastPublishedMessage;
46 | $this->numberOfSubscribers = $numberOfSubscribers;
47 | $this->lastMessageIdentifier = $lastMessageIdentifier;
48 | }
49 |
50 | /**
51 | * The json must look like this:
52 | * {
53 | * "messages": 10,
54 | * "requested": 1,
55 | * "subscribers": 100,
56 | * "last_message_id": "1504818382:1"
57 | * }
58 | *
59 | * @throws NchanException
60 | */
61 | public static function fromJson(string $json): ChannelInformation
62 | {
63 | $response = json_decode($json, true);
64 |
65 | if (!is_array($response)) {
66 | throw new NchanException('Unable to parse JSON response: ' . json_last_error_msg());
67 | }
68 |
69 | // Check if required keys exists in $response.
70 | if (count(array_diff_key(array_flip(self::REQUIRED_JSON_KEYS), $response)) !== 0) {
71 | throw new NchanException(
72 | sprintf(
73 | 'Unable to parse JSON response: Keys "%s" are required. Keys "%s" exists.',
74 | implode('", "', self::REQUIRED_JSON_KEYS),
75 | implode('", "', array_keys($response))
76 | )
77 | );
78 | }
79 |
80 | return new self(
81 | (int)$response['messages'],
82 | (int)$response['requested'],
83 | (int)$response['subscribers'],
84 | (string)$response['last_message_id']
85 | );
86 | }
87 |
88 | /**
89 | * @return mixed
90 | */
91 | public function __get(string $name)
92 | {
93 | return $this->$name;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Api/Model/JsonMessage.php:
--------------------------------------------------------------------------------
1 | name = $name;
16 | $this->content = $content;
17 | }
18 |
19 | public function name(): string
20 | {
21 | return $this->name;
22 | }
23 |
24 | public function content(): string
25 | {
26 | return $this->content;
27 | }
28 |
29 | abstract public function contentType(): string;
30 | }
31 |
--------------------------------------------------------------------------------
/src/Api/Model/PlainTextMessage.php:
--------------------------------------------------------------------------------
1 | numberOfTotalPublishedMessages = $numberOfTotalPublishedMessages;
111 | $this->numberOfStoredMessages = $numberOfStoredMessages;
112 | $this->sharedMemoryUsedInKilobyte = $sharedMemoryUsedInKilobyte;
113 | $this->numberOfChannels = $numberOfChannels;
114 | $this->numberOfSubscribers = $numberOfSubscribers;
115 | $this->numberOfPendingRedisCommands = $numberOfPendingRedisCommands;
116 | $this->numberOfConnectedRedisServers = $numberOfConnectedRedisServers;
117 | $this->numberOfTotalReceivedInterprocessAlerts = $numberOfTotalReceivedInterprocessAlerts;
118 | $this->numberOfInterprocessAlertsInTransit = $numberOfInterprocessAlertsInTransit;
119 | $this->numberOfQueuedInterprocessAlerts = $numberOfQueuedInterprocessAlerts;
120 | $this->totalInterprocessSendDelayInSeconds = $totalInterprocessSendDelayInSeconds;
121 | $this->totalInterprocessReceiveDelayInSeconds = $totalInterprocessReceiveDelayInSeconds;
122 | }
123 |
124 | /**
125 | * The plain text must look like this:
126 | * total published messages: 3
127 | * stored messages: 3
128 | * shared memory used: 16K
129 | * channels: 2
130 | * subscribers: 2
131 | * redis pending commands: 0
132 | * redis connected servers: 2
133 | * total interprocess alerts received: 0
134 | * interprocess alerts in transit: 0
135 | * interprocess queued alerts: 0
136 | * total interprocess send delay: 0
137 | * total interprocess receive delay: 0
138 | *
139 | * @throws NchanException
140 | */
141 | public static function fromPlainText(string $plainText): StatusInformation
142 | {
143 | $plainText = trim($plainText);
144 | $lines = explode("\n", $plainText);
145 |
146 | $response = [];
147 | foreach ($lines as $line) {
148 | // Appending ': ' prevents error if no ': ' exists.
149 | [$key, $value] = explode(': ', trim($line) . ': ');
150 | $response[$key] = $value;
151 | }
152 |
153 | // Check if required keys exists in $response.
154 | if (count(array_diff_key(array_flip(self::REQUIRED_PLAIN_TEXT_KEYS), $response)) !== 0) {
155 | throw new NchanException(
156 | sprintf(
157 | 'Unable to parse status information: Keys "%s" are required. Keys "%s" exists.',
158 | implode('", "', self::REQUIRED_PLAIN_TEXT_KEYS),
159 | implode('", "', array_keys($response))
160 | )
161 | );
162 | }
163 |
164 | return new self(
165 | (int)$response['total published messages'],
166 | (int)$response['stored messages'],
167 | (int)$response['shared memory used'],
168 | (int)$response['channels'],
169 | (int)$response['subscribers'],
170 | (int)$response['redis pending commands'],
171 | (int)$response['redis connected servers'],
172 | (int)$response['total interprocess alerts received'],
173 | (int)$response['interprocess alerts in transit'],
174 | (int)$response['interprocess queued alerts'],
175 | (int)$response['total interprocess send delay'],
176 | (int)$response['total interprocess receive delay']
177 | );
178 | }
179 |
180 | /**
181 | * @return mixed
182 | */
183 | public function __get(string $name)
184 | {
185 | return $this->$name;
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/Api/Model/XmlMessage.php:
--------------------------------------------------------------------------------
1 | statusUrl = $statusUrl;
25 | $this->client = new ThrowExceptionIfRequestRequiresAuthenticationClient(
26 | $client
27 | );
28 | }
29 |
30 | /**
31 | * @throws AuthenticationRequiredException
32 | * @throws NchanException
33 | */
34 | public function information(): StatusInformation
35 | {
36 | $response = $this->client->get(
37 | new Request(
38 | $this->statusUrl,
39 | [
40 | 'Accept' => 'text/plain'
41 | ]
42 | )
43 | );
44 |
45 | if ($response->statusCode() !== Response::OK) {
46 | throw new NchanException(
47 | sprintf(
48 | 'Unable to retrieve status information. HTTP status code was %s.',
49 | $response->statusCode()
50 | )
51 | );
52 | }
53 |
54 | return StatusInformation::fromPlainText($response->body());
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Exception/AuthenticationRequiredException.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | private array $headers;
15 |
16 | private string $body;
17 |
18 | /**
19 | * @param array $headers
20 | */
21 | public function __construct(Url $url, array $headers, string $body = '')
22 | {
23 | $this->url = $url;
24 | $this->headers = $headers;
25 | $this->body = $body;
26 | }
27 |
28 | public function url(): Url
29 | {
30 | return $this->url;
31 | }
32 |
33 | /**
34 | * @return array
35 | */
36 | public function headers(): array
37 | {
38 | return $this->headers;
39 | }
40 |
41 | public function body(): string
42 | {
43 | return $this->body;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Http/Response.php:
--------------------------------------------------------------------------------
1 | client = $client;
16 | }
17 |
18 | public function get(Request $request): Response
19 | {
20 | return $this->throwExceptionIfRequestRequiresAuthenticationOrReturnResponse(
21 | 'GET',
22 | $request,
23 | $this->client->get($request)
24 | );
25 | }
26 |
27 | public function post(Request $request): Response
28 | {
29 | return $this->throwExceptionIfRequestRequiresAuthenticationOrReturnResponse(
30 | 'POST',
31 | $request,
32 | $this->client->post($request)
33 | );
34 | }
35 |
36 | public function delete(Request $request): Response
37 | {
38 | return $this->throwExceptionIfRequestRequiresAuthenticationOrReturnResponse(
39 | 'DELETE',
40 | $request,
41 | $this->client->delete($request)
42 | );
43 | }
44 |
45 | /**
46 | * @throws AuthenticationRequiredException
47 | */
48 | private function throwExceptionIfRequestRequiresAuthenticationOrReturnResponse(
49 | string $method,
50 | Request $request,
51 | Response $response
52 | ): Response {
53 | if ($response->statusCode() === Response::FORBIDDEN) {
54 | throw new AuthenticationRequiredException(
55 | sprintf(
56 | 'Request to "%s %s" requires authentication.',
57 | $method,
58 | $request->url()
59 | )
60 | );
61 | }
62 |
63 | return $response;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Http/Url.php:
--------------------------------------------------------------------------------
1 | value = $value;
39 | }
40 |
41 | /**
42 | * @throws InvalidUrlException
43 | */
44 | public function append(string $value): Url
45 | {
46 | return new Url($this . $value);
47 | }
48 |
49 | public function __toString(): string
50 | {
51 | return $this->toString();
52 | }
53 |
54 | public function toString(): string
55 | {
56 | return $this->value;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/HttpAdapter/BasicAuthenticationCredentials.php:
--------------------------------------------------------------------------------
1 | username = $username;
18 | $this->password = $password;
19 | }
20 |
21 | public function authenticate(Request $request): Request
22 | {
23 | $headers = $request->headers();
24 |
25 | $encodedCredentials = base64_encode($this->username . ':' . $this->password);
26 |
27 | $headers['Authorization'] = 'Basic ' . $encodedCredentials;
28 |
29 | return new Request(
30 | $request->url(),
31 | $headers,
32 | $request->body()
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/HttpAdapter/BearerAuthenticationCredentials.php:
--------------------------------------------------------------------------------
1 | token = $token;
16 | }
17 |
18 | public function authenticate(Request $request): Request
19 | {
20 | $headers = $request->headers();
21 |
22 | $headers['Authorization'] = 'Bearer ' . $this->token;
23 |
24 | return new Request(
25 | $request->url(),
26 | $headers,
27 | $request->body()
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/HttpAdapter/Credentials.php:
--------------------------------------------------------------------------------
1 | credentials = $credentials;
19 | }
20 |
21 | public static function withDefaults(): HttpStreamWrapperClient
22 | {
23 | return new self(
24 | new WithoutAuthenticationCredentials()
25 | );
26 | }
27 |
28 | public function get(Request $request): Response
29 | {
30 | return $this->request('GET', $request);
31 | }
32 |
33 | public function post(Request $request): Response
34 | {
35 | return $this->request('POST', $request);
36 | }
37 |
38 | public function delete(Request $request): Response
39 | {
40 | return $this->request('DELETE', $request);
41 | }
42 |
43 | /**
44 | * @throws NchanException
45 | */
46 | private function request(string $method, Request $request): Response
47 | {
48 | $request = $this->credentials->authenticate($request);
49 |
50 | $url = $request->url()->toString();
51 | $headers = $request->headers();
52 | $body = $request->body();
53 |
54 | $options = [
55 | 'http' =>
56 | [
57 | 'method' => $method,
58 | 'header' => $this->prepareHeadersForStreamContext($headers),
59 | 'content' => $body,
60 | 'ignore_errors' => true
61 | ]
62 | ];
63 |
64 | $context = stream_context_create($options);
65 |
66 | // Suppress errors for file_get_contents. We will analyze this ourselves.
67 | $errorReportingLevelBeforeFileGetContents = error_reporting(0);
68 |
69 | $responseBody = file_get_contents(
70 | $url,
71 | false,
72 | $context
73 | );
74 |
75 | error_reporting($errorReportingLevelBeforeFileGetContents);
76 |
77 | if ($responseBody === false) {
78 | throw new NchanException(
79 | error_get_last()['message'] ?? 'Unable to connect to ' . $url . '.'
80 | );
81 | }
82 |
83 | return HttpStreamWrapperResponse::fromResponse($http_response_header, $responseBody);
84 | }
85 |
86 | /**
87 | * Transforms the array from
88 | * [
89 | * 'firstHeaderName' => 'firstHeaderValue',
90 | * 'secondHeaderName' => 'secondHeaderValue'
91 | * ]
92 | * to string
93 | * "firstHeaderName: firstHeaderValue\r\n
94 | * secondHeaderName: secondHeaderValue".
95 | *
96 | * @param array $headers
97 | */
98 | private function prepareHeadersForStreamContext(array $headers): string
99 | {
100 | return implode(
101 | "\r\n",
102 | array_map(
103 | static fn(string $name, string $value) => $name . ': ' . $value,
104 | array_keys($headers),
105 | $headers
106 | )
107 | );
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/HttpAdapter/HttpStreamWrapperResponse.php:
--------------------------------------------------------------------------------
1 | statusCode = $statusCode;
19 | $this->body = $body;
20 | }
21 |
22 | /**
23 | * @param array $headers The result from $http_response_header.
24 | *
25 | * @throws NchanException
26 | */
27 | public static function fromResponse(array $headers, string $body): HttpStreamWrapperResponse
28 | {
29 | // The first array value is for example "HTTP\1.1 200 OK" and must be set.
30 | if (!isset($headers[0])) {
31 | throw new NchanException('Unable to parse response header.');
32 | }
33 |
34 | // Get the status code.
35 | preg_match('/HTTP[^ ]+ (\d+)/', $headers[0], $matches);
36 |
37 | if (!isset($matches[1])) {
38 | throw new NchanException('Unable to retrieve status code from response header.');
39 | }
40 |
41 | $statusCode = (int)$matches[1];
42 |
43 | return new self(
44 | $statusCode,
45 | $body
46 | );
47 | }
48 |
49 | public function statusCode(): int
50 | {
51 | return $this->statusCode;
52 | }
53 |
54 | public function body(): string
55 | {
56 | return $this->body;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/HttpAdapter/Psr18ClientAdapter.php:
--------------------------------------------------------------------------------
1 | client = $client;
30 | $this->requestFactory = $requestFactory;
31 | $this->streamFactory = $streamFactory;
32 | }
33 |
34 | public function get(Request $request): Response
35 | {
36 | return $this->request('GET', $request);
37 | }
38 |
39 | public function post(Request $request): Response
40 | {
41 | return $this->request('POST', $request);
42 | }
43 |
44 | public function delete(Request $request): Response
45 | {
46 | return $this->request('DELETE', $request);
47 | }
48 |
49 | /**
50 | * @throws NchanException
51 | */
52 | private function request(string $method, Request $request): Response
53 | {
54 | try {
55 | $psrRequest = $this->requestFactory
56 | ->createRequest($method, $request->url()->toString())
57 | ->withBody($this->streamFactory->createStream($request->body()));
58 |
59 | foreach ($request->headers() as $name => $value) {
60 | $psrRequest = $psrRequest->withHeader($name, $value);
61 | }
62 |
63 | return new Psr7ResponseAdapter($this->client->sendRequest($psrRequest));
64 | } catch (ClientExceptionInterface $exception) {
65 | throw new NchanException(
66 | $exception->getMessage(),
67 | $exception->getCode(),
68 | $exception
69 | );
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/HttpAdapter/Psr7ResponseAdapter.php:
--------------------------------------------------------------------------------
1 | response = $response;
17 | }
18 |
19 | public function statusCode(): int
20 | {
21 | return $this->response->getStatusCode();
22 | }
23 |
24 | public function body(): string
25 | {
26 | return (string)$this->response->getBody();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/HttpAdapter/WithoutAuthenticationCredentials.php:
--------------------------------------------------------------------------------
1 | baseUrl = new Url($baseUrl);
26 | $this->client = $client ?? HttpStreamWrapperClient::withDefaults();
27 | }
28 |
29 | /**
30 | * @throws InvalidUrlException
31 | */
32 | public function channel(string $name): Channel
33 | {
34 | return new Channel(
35 | $this->baseUrl->append($name),
36 | $this->client
37 | );
38 | }
39 |
40 | /**
41 | * The path must be configured with the "nchan_stub_status;" directive.
42 | *
43 | * @throws InvalidUrlException
44 | */
45 | public function status(string $path): Status
46 | {
47 | return new Status(
48 | $this->baseUrl->append($path),
49 | $this->client
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Integration/HttpAdapter/ClientTestCase.php:
--------------------------------------------------------------------------------
1 | 'application/json'
26 | ]
27 | );
28 |
29 | $client = $this->createClient($request);
30 |
31 | $response = $client->get($request);
32 |
33 | $serverResponse = unserialize($response->body());
34 |
35 | $this->assertSame(201, $response->statusCode());
36 | $this->assertSame('application/json', $serverResponse['SERVER']['HTTP_ACCEPT']);
37 | }
38 |
39 | /**
40 | * @test
41 | */
42 | public function itShouldPerformPostRequest(): void
43 | {
44 | $request = new Request(
45 | new Url(getenv('INTEGRATION_TEST_BASE_URL') . '?statusCode=201'),
46 | [
47 | 'Accept' => 'application/json',
48 | 'Content-Type' => 'application/x-www-form-urlencoded'
49 | ],
50 | http_build_query(
51 | [
52 | 'message' => 'my-message-name'
53 | ]
54 | )
55 | );
56 |
57 | $client = $this->createClient($request);
58 |
59 | $response = $client->post($request);
60 |
61 | $serverResponse = unserialize($response->body());
62 |
63 | $this->assertSame(201, $response->statusCode());
64 | $this->assertSame('application/json', $serverResponse['SERVER']['HTTP_ACCEPT']);
65 | $this->assertSame('my-message-name', $serverResponse['POST']['message']);
66 | }
67 |
68 | /**
69 | * @test
70 | */
71 | public function itShouldPerformDeleteRequest(): void
72 | {
73 | $request = new Request(
74 | new Url(getenv('INTEGRATION_TEST_BASE_URL') . '?statusCode=201'),
75 | [
76 | 'Accept' => 'application/json'
77 | ]
78 | );
79 |
80 | $client = $this->createClient($request);
81 |
82 | $response = $client->delete($request);
83 |
84 | $serverResponse = unserialize($response->body());
85 |
86 | $this->assertSame(201, $response->statusCode());
87 | $this->assertSame('application/json', $serverResponse['SERVER']['HTTP_ACCEPT']);
88 | }
89 |
90 | /**
91 | * @test
92 | */
93 | public function itShouldThrowExceptionOnInvalidResponse(): void
94 | {
95 | $this->expectException(NchanException::class);
96 |
97 | $request = new Request(
98 | new Url(getenv('INTEGRATION_TEST_INVALID_BASE_URL')),
99 | [
100 | 'Accept' => 'application/json'
101 | ]
102 | );
103 |
104 | $client = $this->createClient($request);
105 |
106 | $client->get($request);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tests/Integration/HttpAdapter/HttpStreamWrapperClientTest.php:
--------------------------------------------------------------------------------
1 | createMock(Credentials::class);
17 | $credentials->expects($this->once())->method('authenticate')->willReturn($request);
18 |
19 | return new HttpStreamWrapperClient($credentials);
20 | }
21 |
22 | /**
23 | * @test
24 | */
25 | public function itShouldBeCreatedWithDefaults(): void
26 | {
27 | HttpStreamWrapperClient::withDefaults();
28 |
29 | $this->assertTrue(true);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Integration/HttpAdapter/Psr18ClientAdapterTest.php:
--------------------------------------------------------------------------------
1 | suiteName = $suiteName;
41 | $this->socket = $socket;
42 | $this->documentRoot = $documentRoot;
43 | }
44 |
45 | /**
46 | * @inheritdoc
47 | */
48 | public function startTestSuite(TestSuite $suite): void
49 | {
50 | if ($suite->getName() === $this->suiteName) {
51 | $process = new Process(
52 | [
53 | 'php',
54 | '-S',
55 | $this->socket,
56 | $this->documentRoot
57 | ]
58 | );
59 | $process->start();
60 |
61 | // Wait for the server.
62 | sleep(1);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/TestServer/index.php:
--------------------------------------------------------------------------------
1 | $_GET,
14 | 'POST' => $_POST,
15 | 'SERVER' => $_SERVER
16 | ]
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Unit/Api/ChannelTest.php:
--------------------------------------------------------------------------------
1 | 'text/plain',
31 | 'Accept' => 'application/json',
32 | 'X-EventSource-Event' => 'my-message-name'
33 | ],
34 | 'my message content'
35 | );
36 |
37 | $response = $this->createMock(Response::class);
38 | $response->method('body')->willReturn(
39 | '{"messages": 10, "requested": 1, "subscribers": 100, "last_message_id": "1504818382:1"}'
40 | );
41 | $response->method('statusCode')->willReturn($statusCode);
42 |
43 | $client = $this->createMock(Client::class);
44 | $client->method('post')->with($expectedRequest)->willReturn($response);
45 |
46 | $message = $this->createMock(Message::class);
47 | $message->method('contentType')->willReturn('text/plain');
48 | $message->method('name')->willReturn('my-message-name');
49 | $message->method('content')->willReturn('my message content');
50 |
51 | $channel = new Channel(new Url('http://localhost'), $client);
52 | $information = $channel->publish($message);
53 |
54 | $this->assertInstanceOf(ChannelInformation::class, $information);
55 | }
56 | }
57 |
58 | /**
59 | * @test
60 | */
61 | public function itShouldReturnInformation(): void
62 | {
63 | $expectedRequest = new Request(
64 | new Url('http://localhost'),
65 | [
66 | 'Accept' => 'application/json',
67 | ]
68 | );
69 |
70 | $response = $this->createMock(Response::class);
71 | $response->method('body')->willReturn(
72 | '{"messages": 10, "requested": 1, "subscribers": 100, "last_message_id": "1504818382:1"}'
73 | );
74 | $response->method('statusCode')->willReturn(200);
75 |
76 | $client = $this->createMock(Client::class);
77 | $client->method('get')->with($expectedRequest)->willReturn($response);
78 |
79 | $channel = new Channel(new Url('http://localhost'), $client);
80 | $information = $channel->information();
81 |
82 | $this->assertInstanceOf(ChannelInformation::class, $information);
83 | }
84 |
85 | /**
86 | * @test
87 | */
88 | public function itShouldDeleteChannel(): void
89 | {
90 | // 200 and 404 are valid status codes
91 | foreach ([200, 404] as $statusCode) {
92 | $expectedRequest = new Request(
93 | new Url('http://localhost'),
94 | [
95 | 'Accept' => 'application/json',
96 | ]
97 | );
98 |
99 | $response = $this->createMock(Response::class);
100 | $response->method('statusCode')->willReturn($statusCode);
101 |
102 | $client = $this->createMock(Client::class);
103 | $client->method('delete')->with($expectedRequest)->willReturn($response);
104 |
105 | $channel = new Channel(new Url('http://localhost'), $client);
106 | $channel->delete();
107 |
108 | // Test passed
109 | $this->assertTrue(true);
110 | }
111 | }
112 |
113 | /**
114 | * @test
115 | */
116 | public function publishShouldThrowExceptionOnNotExpectedStatusCode()
117 | {
118 | $this->expectException(NchanException::class);
119 |
120 | $response = $this->createMock(Response::class);
121 | $response->method('statusCode')->willReturn(302);
122 |
123 | $client = $this->createMock(Client::class);
124 | $client->method('post')->willReturn($response);
125 |
126 | $message = $this->createMock(Message::class);
127 |
128 | $channel = new Channel(new Url('http://localhost'), $client);
129 | $channel->publish($message);
130 | }
131 |
132 | /**
133 | * @test
134 | */
135 | public function informationShouldThrowExceptionOnNotExpectedStatusCode()
136 | {
137 | $this->expectException(NchanException::class);
138 |
139 | $response = $this->createMock(Response::class);
140 | $response->method('statusCode')->willReturn(302);
141 |
142 | $client = $this->createMock(Client::class);
143 | $client->method('get')->willReturn($response);
144 |
145 | $channel = new Channel(new Url('http://localhost'), $client);
146 | $channel->information();
147 | }
148 |
149 | /**
150 | * @test
151 | */
152 | public function deleteShouldThrowExceptionOnNotExpectedStatusCode()
153 | {
154 | $this->expectException(NchanException::class);
155 |
156 | $response = $this->createMock(Response::class);
157 | $response->method('statusCode')->willReturn(302);
158 |
159 | $client = $this->createMock(Client::class);
160 | $client->method('delete')->willReturn($response);
161 |
162 | $channel = new Channel(new Url('http://localhost'), $client);
163 | $channel->delete();
164 | }
165 |
166 | /**
167 | * @test
168 | */
169 | public function publishShouldThrowExceptionIfForbidden()
170 | {
171 | $this->expectException(AuthenticationRequiredException::class);
172 |
173 | $response = $this->createMock(Response::class);
174 | $response->method('statusCode')->willReturn(403);
175 |
176 | $client = $this->createMock(Client::class);
177 | $client->method('post')->willReturn($response);
178 |
179 | $message = $this->createMock(Message::class);
180 |
181 | $channel = new Channel(new Url('http://localhost'), $client);
182 | $channel->publish($message);
183 | }
184 |
185 | /**
186 | * @test
187 | */
188 | public function informationShouldThrowExceptionIfForbidden()
189 | {
190 | $this->expectException(AuthenticationRequiredException::class);
191 |
192 | $response = $this->createMock(Response::class);
193 | $response->method('statusCode')->willReturn(403);
194 |
195 | $client = $this->createMock(Client::class);
196 | $client->method('get')->willReturn($response);
197 |
198 | $channel = new Channel(new Url('http://localhost'), $client);
199 | $channel->information();
200 | }
201 |
202 | /**
203 | * @test
204 | */
205 | public function deleteShouldThrowExceptionIfForbidden()
206 | {
207 | $this->expectException(AuthenticationRequiredException::class);
208 |
209 | $response = $this->createMock(Response::class);
210 | $response->method('statusCode')->willReturn(403);
211 |
212 | $client = $this->createMock(Client::class);
213 | $client->method('delete')->willReturn($response);
214 |
215 | $channel = new Channel(new Url('http://localhost'), $client);
216 | $channel->delete();
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/tests/Unit/Api/Model/ChannelInformationTest.php:
--------------------------------------------------------------------------------
1 | validJson();
19 |
20 | $channelInformation = ChannelInformation::fromJson($json);
21 |
22 | $this->assertSame(10, $channelInformation->numberOfMessages);
23 | $this->assertSame(1, $channelInformation->secondsSinceLastPublishedMessage);
24 | $this->assertSame(100, $channelInformation->numberOfSubscribers);
25 | $this->assertSame('1504818382:1', $channelInformation->lastMessageIdentifier);
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function itShouldBeThrowAnExceptionWhenJsonIsInvalid(): void
32 | {
33 | $this->expectException(NchanException::class);
34 |
35 | $json = 'malformed json"}';
36 |
37 | ChannelInformation::fromJson($json);
38 | }
39 |
40 | /**
41 | * @test
42 | * @dataProvider missingKeysProvider
43 | */
44 | public function itShouldBeThrowAnExceptionWhenJsonHasMissingKeys(string $json): void
45 | {
46 | $this->expectException(NchanException::class);
47 |
48 | ChannelInformation::fromJson($json);
49 | }
50 |
51 | /**
52 | * Returns various json with missing keys.
53 | *
54 | * @return array
55 | */
56 | public function missingKeysProvider(): array
57 | {
58 | return [
59 | ['{"requested": 1, "subscribers": 100, "last_message_id": "1504818382:1"}'],
60 | ['{"messages": 10, "subscribers": 100, "last_message_id": "1504818382:1"}'],
61 | ['{"messages": 10, "requested": 1, "last_message_id": "1504818382:1"}'],
62 | ['{"messages": 10, "requested": 1, "subscribers": 100}']
63 | ];
64 | }
65 |
66 | /**
67 | * Returns a valid json for tests.
68 | *
69 | * @return string
70 | */
71 | private function validJson(): string
72 | {
73 | return '{"messages": 10, "requested": 1, "subscribers": 100, "last_message_id": "1504818382:1"}';
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tests/Unit/Api/Model/JsonMessageTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedName, $message->name());
24 | $this->assertEquals($expectedContent, $message->content());
25 | $this->assertEquals($expectedContentType, $message->contentType());
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function itShouldBeCreatedWithEmptyName(): void
32 | {
33 | $expectedName = '';
34 | $expectedContent = 'my message content';
35 | $expectedContentType = 'application/json';
36 |
37 | $message = new JsonMessage($expectedName, $expectedContent);
38 |
39 | $this->assertEquals($expectedName, $message->name());
40 | $this->assertEquals($expectedContent, $message->content());
41 | $this->assertEquals($expectedContentType, $message->contentType());
42 | }
43 |
44 | /**
45 | * @test
46 | */
47 | public function itShouldBeCreatedWithEmptyContent(): void
48 | {
49 | $expectedName = 'my-message-name';
50 | $expectedContent = '';
51 | $expectedContentType = 'application/json';
52 |
53 | $message = new JsonMessage($expectedName, $expectedContent);
54 |
55 | $this->assertEquals($expectedName, $message->name());
56 | $this->assertEquals($expectedContent, $message->content());
57 | $this->assertEquals($expectedContentType, $message->contentType());
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Unit/Api/Model/PlainTextMessageTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedName, $message->name());
24 | $this->assertEquals($expectedContent, $message->content());
25 | $this->assertEquals($expectedContentType, $message->contentType());
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function itShouldBeCreatedWithEmptyName(): void
32 | {
33 | $expectedName = '';
34 | $expectedContent = 'my message content';
35 | $expectedContentType = 'text/plain';
36 |
37 | $message = new PlainTextMessage($expectedName, $expectedContent);
38 |
39 | $this->assertEquals($expectedName, $message->name());
40 | $this->assertEquals($expectedContent, $message->content());
41 | $this->assertEquals($expectedContentType, $message->contentType());
42 | }
43 |
44 | /**
45 | * @test
46 | */
47 | public function itShouldBeCreatedWithEmptyContent(): void
48 | {
49 | $expectedName = 'my-message-name';
50 | $expectedContent = '';
51 | $expectedContentType = 'text/plain';
52 |
53 | $message = new PlainTextMessage($expectedName, $expectedContent);
54 |
55 | $this->assertEquals($expectedName, $message->name());
56 | $this->assertEquals($expectedContent, $message->content());
57 | $this->assertEquals($expectedContentType, $message->contentType());
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Unit/Api/Model/StatusInformationTest.php:
--------------------------------------------------------------------------------
1 | validPlainText();
19 |
20 | $statusInformation = StatusInformation::fromPlainText($plainText);
21 |
22 | $this->assertSame(1, $statusInformation->numberOfTotalPublishedMessages);
23 | $this->assertSame(2, $statusInformation->numberOfStoredMessages);
24 | $this->assertSame(3, $statusInformation->sharedMemoryUsedInKilobyte);
25 | $this->assertSame(4, $statusInformation->numberOfChannels);
26 | $this->assertSame(5, $statusInformation->numberOfSubscribers);
27 | $this->assertSame(6, $statusInformation->numberOfPendingRedisCommands);
28 | $this->assertSame(7, $statusInformation->numberOfConnectedRedisServers);
29 | $this->assertSame(8, $statusInformation->numberOfTotalReceivedInterprocessAlerts);
30 | $this->assertSame(9, $statusInformation->numberOfInterprocessAlertsInTransit);
31 | $this->assertSame(10, $statusInformation->numberOfQueuedInterprocessAlerts);
32 | $this->assertSame(11, $statusInformation->totalInterprocessSendDelayInSeconds);
33 | $this->assertSame(12, $statusInformation->totalInterprocessReceiveDelayInSeconds);
34 | }
35 |
36 | /**
37 | * @test
38 | */
39 | public function itShouldBeThrowAnExceptionWhenPlainTextIsInvalid(): void
40 | {
41 | $this->expectException(NchanException::class);
42 |
43 | $plainText = 'malformed plain text';
44 |
45 | StatusInformation::fromPlainText($plainText);
46 | }
47 |
48 | /**
49 | * @test
50 | * @dataProvider missingKeysProvider
51 | */
52 | public function itShouldBeThrowAnExceptionWhenPlainTextHasMissingKeys(string $plainText): void
53 | {
54 | $this->expectException(NchanException::class);
55 |
56 | StatusInformation::fromPlainText($plainText);
57 | }
58 |
59 | /**
60 | * Returns various plain text with missing keys.
61 | *
62 | * @return array
63 | */
64 | public function missingKeysProvider(): array
65 | {
66 | return [
67 | [
68 | '
69 | stored messages: 2
70 | shared memory used: 3K
71 | channels: 4
72 | subscribers: 5
73 | redis pending commands: 6
74 | redis connected servers: 7
75 | total interprocess alerts received: 8
76 | interprocess alerts in transit: 9
77 | interprocess queued alerts: 10
78 | total interprocess send delay: 11
79 | total interprocess receive delay: 12
80 | '
81 | ],
82 | [
83 | '
84 | total published messages: 1
85 | shared memory used: 3K
86 | channels: 4
87 | subscribers: 5
88 | redis pending commands: 6
89 | redis connected servers: 7
90 | total interprocess alerts received: 8
91 | interprocess alerts in transit: 9
92 | interprocess queued alerts: 10
93 | total interprocess send delay: 11
94 | total interprocess receive delay: 12
95 | '
96 | ],
97 | [
98 | '
99 | total published messages: 1
100 | stored messages: 2
101 | channels: 4
102 | subscribers: 5
103 | redis pending commands: 6
104 | redis connected servers: 7
105 | total interprocess alerts received: 8
106 | interprocess alerts in transit: 9
107 | interprocess queued alerts: 10
108 | total interprocess send delay: 11
109 | total interprocess receive delay: 12
110 | '
111 | ],
112 | [
113 | '
114 | total published messages: 1
115 | stored messages: 2
116 | shared memory used: 3K
117 | subscribers: 5
118 | redis pending commands: 6
119 | redis connected servers: 7
120 | total interprocess alerts received: 8
121 | interprocess alerts in transit: 9
122 | interprocess queued alerts: 10
123 | total interprocess send delay: 11
124 | total interprocess receive delay: 12
125 | '
126 | ],
127 | [
128 | '
129 | total published messages: 1
130 | stored messages: 2
131 | shared memory used: 3K
132 | channels: 4
133 | redis pending commands: 6
134 | redis connected servers: 7
135 | total interprocess alerts received: 8
136 | interprocess alerts in transit: 9
137 | interprocess queued alerts: 10
138 | total interprocess send delay: 11
139 | total interprocess receive delay: 12
140 | '
141 | ],
142 | [
143 | '
144 | total published messages: 1
145 | stored messages: 2
146 | shared memory used: 3K
147 | channels: 4
148 | subscribers: 5
149 | redis connected servers: 7
150 | total interprocess alerts received: 8
151 | interprocess alerts in transit: 9
152 | interprocess queued alerts: 10
153 | total interprocess send delay: 11
154 | total interprocess receive delay: 12
155 | '
156 | ],
157 | [
158 | '
159 | total published messages: 1
160 | stored messages: 2
161 | shared memory used: 3K
162 | channels: 4
163 | subscribers: 5
164 | redis pending commands: 6
165 | total interprocess alerts received: 8
166 | interprocess alerts in transit: 9
167 | interprocess queued alerts: 10
168 | total interprocess send delay: 11
169 | total interprocess receive delay: 12
170 | '
171 | ],
172 | [
173 | '
174 | total published messages: 1
175 | stored messages: 2
176 | shared memory used: 3K
177 | channels: 4
178 | subscribers: 5
179 | redis pending commands: 6
180 | redis connected servers: 7
181 | interprocess alerts in transit: 9
182 | interprocess queued alerts: 10
183 | total interprocess send delay: 11
184 | total interprocess receive delay: 12
185 | '
186 | ],
187 | [
188 | '
189 | total published messages: 1
190 | stored messages: 2
191 | shared memory used: 3K
192 | channels: 4
193 | subscribers: 5
194 | redis pending commands: 6
195 | redis connected servers: 7
196 | total interprocess alerts received: 8
197 | interprocess queued alerts: 10
198 | total interprocess send delay: 11
199 | total interprocess receive delay: 12
200 | '
201 | ],
202 | [
203 | '
204 | total published messages: 1
205 | stored messages: 2
206 | shared memory used: 3K
207 | channels: 4
208 | subscribers: 5
209 | redis pending commands: 6
210 | redis connected servers: 7
211 | total interprocess alerts received: 8
212 | interprocess alerts in transit: 9
213 | total interprocess send delay: 11
214 | total interprocess receive delay: 12
215 | '
216 | ],
217 | [
218 | '
219 | total published messages: 1
220 | stored messages: 2
221 | shared memory used: 3K
222 | channels: 4
223 | subscribers: 5
224 | redis pending commands: 6
225 | redis connected servers: 7
226 | total interprocess alerts received: 8
227 | interprocess alerts in transit: 9
228 | interprocess queued alerts: 10
229 | total interprocess receive delay: 12
230 | '
231 | ],
232 | [
233 | '
234 | total published messages: 1
235 | stored messages: 2
236 | shared memory used: 3K
237 | channels: 4
238 | subscribers: 5
239 | redis pending commands: 6
240 | redis connected servers: 7
241 | total interprocess alerts received: 8
242 | interprocess alerts in transit: 9
243 | interprocess queued alerts: 10
244 | total interprocess send delay: 11
245 | '
246 | ]
247 | ];
248 | }
249 |
250 | /**
251 | * Returns a valid plain text for tests.
252 | *
253 | * @return string
254 | */
255 | private function validPlainText(): string
256 | {
257 | return <<< STRING
258 | total published messages: 1
259 | stored messages: 2
260 | shared memory used: 3K
261 | channels: 4
262 | subscribers: 5
263 | redis pending commands: 6
264 | redis connected servers: 7
265 | total interprocess alerts received: 8
266 | interprocess alerts in transit: 9
267 | interprocess queued alerts: 10
268 | total interprocess send delay: 11
269 | total interprocess receive delay: 12
270 | STRING;
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/tests/Unit/Api/Model/XmlMessageTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedName, $message->name());
24 | $this->assertEquals($expectedContent, $message->content());
25 | $this->assertEquals($expectedContentType, $message->contentType());
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function itShouldBeCreatedWithEmptyName(): void
32 | {
33 | $expectedName = '';
34 | $expectedContent = 'my message content';
35 | $expectedContentType = 'application/xml';
36 |
37 | $message = new XmlMessage($expectedName, $expectedContent);
38 |
39 | $this->assertEquals($expectedName, $message->name());
40 | $this->assertEquals($expectedContent, $message->content());
41 | $this->assertEquals($expectedContentType, $message->contentType());
42 | }
43 |
44 | /**
45 | * @test
46 | */
47 | public function itShouldBeCreatedWithEmptyContent(): void
48 | {
49 | $expectedName = 'my-message-name';
50 | $expectedContent = '';
51 | $expectedContentType = 'application/xml';
52 |
53 | $message = new XmlMessage($expectedName, $expectedContent);
54 |
55 | $this->assertEquals($expectedName, $message->name());
56 | $this->assertEquals($expectedContent, $message->content());
57 | $this->assertEquals($expectedContentType, $message->contentType());
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Unit/Api/StatusTest.php:
--------------------------------------------------------------------------------
1 | createMock(Response::class);
24 | $response->method('body')->willReturn(
25 | <<< STRING
26 | total published messages: 1
27 | stored messages: 2
28 | shared memory used: 3K
29 | channels: 4
30 | subscribers: 5
31 | redis pending commands: 6
32 | redis connected servers: 7
33 | total interprocess alerts received: 8
34 | interprocess alerts in transit: 9
35 | interprocess queued alerts: 10
36 | total interprocess send delay: 11
37 | total interprocess receive delay: 12
38 | STRING
39 | );
40 | $response->method('statusCode')->willReturn(200);
41 |
42 | $client = $this->createMock(Client::class);
43 | $client->method('get')->willReturn($response);
44 |
45 | $status = new Status(new Url('http://localhost'), $client);
46 | $information = $status->information();
47 |
48 | $this->assertInstanceOf(StatusInformation::class, $information);
49 | }
50 |
51 | /**
52 | * @test
53 | */
54 | public function informationShouldThrowExceptionIfForbidden()
55 | {
56 | $this->expectException(AuthenticationRequiredException::class);
57 |
58 | $response = $this->createMock(Response::class);
59 | $response->method('statusCode')->willReturn(403);
60 |
61 | $client = $this->createMock(Client::class);
62 | $client->method('get')->willReturn($response);
63 |
64 | $status = new Status(new Url('http://localhost'), $client);
65 | $status->information();
66 | }
67 |
68 | /**
69 | * @test
70 | */
71 | public function informationShouldThrowExceptionOnNotExpectedStatusCode()
72 | {
73 | $this->expectException(NchanException::class);
74 |
75 | $response = $this->createMock(Response::class);
76 | $response->method('statusCode')->willReturn(201);
77 |
78 | $client = $this->createMock(Client::class);
79 | $client->method('get')->willReturn($response);
80 |
81 | $status = new Status(new Url('http://localhost'), $client);
82 | $status->information();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/Unit/Http/RequestTest.php:
--------------------------------------------------------------------------------
1 | 'application/json'];
20 | $expectedBody = 'my body';
21 |
22 | $request = new Request($expectedUrl, $expectedHeaders, $expectedBody);
23 |
24 | $this->assertEquals($expectedUrl, $request->url());
25 | $this->assertEquals($expectedHeaders, $request->headers());
26 | $this->assertEquals($expectedBody, $request->body());
27 | }
28 |
29 | /**
30 | * @test
31 | */
32 | public function itShouldBeCreatedWithEmptyBody(): void
33 | {
34 | $expectedUrl = new Url('http://localhost/my-url');
35 | $expectedHeaders = ['Accept' => 'application/json'];
36 | $expectedBody = '';
37 |
38 | $request = new Request($expectedUrl, $expectedHeaders);
39 |
40 | $this->assertEquals($expectedUrl, $request->url());
41 | $this->assertEquals($expectedHeaders, $request->headers());
42 | $this->assertEquals($expectedBody, $request->body());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Unit/Http/ThrowExceptionIfRequestRequiresAuthenticationClientTest.php:
--------------------------------------------------------------------------------
1 | expectException(AuthenticationRequiredException::class);
23 |
24 | $client = new ThrowExceptionIfRequestRequiresAuthenticationClient(
25 | $this->createForbiddenClient()
26 | );
27 |
28 | $client->get(new Request(new Url('http://localhost'), []));
29 | }
30 |
31 | /**
32 | * @test
33 | */
34 | public function itShouldThrowExceptionOnForbiddenPostRequest(): void
35 | {
36 | $this->expectException(AuthenticationRequiredException::class);
37 |
38 | $client = new ThrowExceptionIfRequestRequiresAuthenticationClient(
39 | $this->createForbiddenClient()
40 | );
41 |
42 | $client->post(new Request(new Url('http://localhost'), []));
43 | }
44 |
45 | /**
46 | * @test
47 | */
48 | public function itShouldThrowExceptionOnForbiddenDeleteRequest(): void
49 | {
50 | $this->expectException(AuthenticationRequiredException::class);
51 |
52 | $client = new ThrowExceptionIfRequestRequiresAuthenticationClient(
53 | $this->createForbiddenClient()
54 | );
55 |
56 | $client->delete(new Request(new Url('http://localhost'), []));
57 | }
58 |
59 | /**
60 | * @return Client
61 | */
62 | private function createForbiddenClient(): Client
63 | {
64 | return new class implements Client {
65 | public function get(Request $request): Response
66 | {
67 | return $this->createForbiddenResponse();
68 | }
69 |
70 | public function post(Request $request): Response
71 | {
72 | return $this->createForbiddenResponse();
73 | }
74 |
75 | public function delete(Request $request): Response
76 | {
77 | return $this->createForbiddenResponse();
78 | }
79 |
80 | private function createForbiddenResponse(): Response
81 | {
82 | return new class implements Response {
83 | public function statusCode(): int
84 | {
85 | return Response::FORBIDDEN;
86 | }
87 |
88 | public function body(): string
89 | {
90 | return '';
91 | }
92 | };
93 | }
94 | };
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/Unit/Http/UrlTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
20 | $url,
21 | (new Url($url))->toString()
22 | );
23 | }
24 |
25 | /**
26 | * @test
27 | * @dataProvider invalidUrlsProvider
28 | */
29 | public function itShouldThrowAnExceptionOnInvalidUrls(string $url): void
30 | {
31 | $this->expectException(InvalidUrlException::class);
32 |
33 | new Url($url);
34 | }
35 |
36 | /**
37 | * @test
38 | */
39 | public function itCanBeAppendedWithString(): void
40 | {
41 | $expectedUrl = 'http://localhost/foo/bar';
42 |
43 | $url = new Url('http://localhost');
44 | $urlCopy = clone $url;
45 |
46 | $newUrl = $url->append('/foo/bar');
47 |
48 | $this->assertEquals($expectedUrl, $newUrl->toString());
49 | # Test immutability
50 | $this->assertEquals($url, $urlCopy);
51 | }
52 |
53 | /**
54 | * @test
55 | */
56 | public function itCanBeTypeCastedToString(): void
57 | {
58 | $expectedUrl = 'http://localhost';
59 |
60 | $url = new Url($expectedUrl);
61 |
62 | $this->assertEquals($expectedUrl, (string)$url);
63 | $this->assertEquals($expectedUrl, $url->toString());
64 | }
65 |
66 | /**
67 | * Returns valid urls.
68 | *
69 | * @return array
70 | */
71 | public function validUrlsProvider(): array
72 | {
73 | return [
74 | ['http://localhost'],
75 | ['https://localhost'],
76 | ['http://localhost/'],
77 | ['https://localhost/'],
78 | ['http://localhost/?foo=bar'],
79 | ['https://localhost/?foo=bar'],
80 | ['http://localhost/foo/bar'],
81 | ['https://localhost/foo/bar']
82 | ];
83 | }
84 |
85 | /**
86 | * Returns invalid urls.
87 | *
88 | * @return array
89 | */
90 | public function invalidUrlsProvider(): array
91 | {
92 | return [
93 | ['/'],
94 | ['/?foo=bar'],
95 | ['/foo/bar'],
96 | ['malformed'],
97 | ['http:///:80']
98 | ];
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/Unit/HttpAdapter/BasicAuthenticationCredentialsTest.php:
--------------------------------------------------------------------------------
1 | authenticate(
26 | new Request(
27 | new Url('http://localhost'),
28 | []
29 | )
30 | );
31 |
32 | $this->assertTrue($request->headers()['Authorization'] === $expectedAuthorizationHeaderValue);
33 | }
34 |
35 | /**
36 | * @test
37 | */
38 | public function itShouldOverrideAnExistingAuthenticationHeader(): void
39 | {
40 | $username = 'starwars';
41 | $password = '25.05.1977';
42 | $credentials = new BasicAuthenticationCredentials($username, $password);
43 |
44 | $expectedAuthorizationHeaderValue = 'Basic ' . base64_encode($username . ':' . $password);
45 |
46 | $request = $credentials->authenticate(
47 | new Request(
48 | new Url('http://localhost'),
49 | [
50 | 'Authorization' => 'Token my-token'
51 | ]
52 | )
53 | );
54 |
55 | $this->assertTrue($request->headers()['Authorization'] === $expectedAuthorizationHeaderValue);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Unit/HttpAdapter/BearerAuthenticationCredentialsTest.php:
--------------------------------------------------------------------------------
1 | authenticate(
25 | new Request(
26 | new Url('http://localhost'),
27 | []
28 | )
29 | );
30 |
31 | $this->assertTrue($request->headers()['Authorization'] === $expectedAuthorizationHeaderValue);
32 | }
33 |
34 | /**
35 | * @test
36 | */
37 | public function itShouldOverrideAnExistingAuthenticationHeader(): void
38 | {
39 | $token = 'my-token';
40 | $credentials = new BearerAuthenticationCredentials($token);
41 |
42 | $expectedAuthorizationHeaderValue = 'Bearer ' . $token;
43 |
44 | $request = $credentials->authenticate(
45 | new Request(
46 | new Url('http://localhost'),
47 | [
48 | 'Authorization' => 'Token other-token'
49 | ]
50 | )
51 | );
52 |
53 | $this->assertTrue($request->headers()['Authorization'] === $expectedAuthorizationHeaderValue);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Unit/HttpAdapter/HttpStreamWrapperResponseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedStatusCode, $response->statusCode());
24 | $this->assertEquals($expectedBody, $response->body());
25 | }
26 |
27 | /**
28 | * @test
29 | */
30 | public function itShouldBeCreatedFromStreamWrapperResponse(): void
31 | {
32 | $expectedStatusCode = 200;
33 | $expectedBody = 'my body';
34 |
35 | $response = HttpStreamWrapperResponse::fromResponse(
36 | [
37 | 'HTTP\1.1 200 OK'
38 | ],
39 | $expectedBody
40 | );
41 |
42 | $this->assertEquals($expectedStatusCode, $response->statusCode());
43 | $this->assertEquals($expectedBody, $response->body());
44 | }
45 |
46 | /**
47 | * @test
48 | * @dataProvider incorrectResponseHeadersProvider
49 | */
50 | public function itShouldThrowAnExceptionWhenIncorrectResponseHeadersArePassed(array $headers): void
51 | {
52 | $this->expectException(NchanException::class);
53 |
54 | HttpStreamWrapperResponse::fromResponse($headers, '');
55 | }
56 |
57 | /**
58 | * Returns various incorrect response headers.
59 | *
60 | * @return array
61 | */
62 | public function incorrectResponseHeadersProvider(): array
63 | {
64 | return [
65 | [['wrong response header']],
66 | [['HTT 200 OK']],
67 | [['HTTP/1.1 OK']],
68 | [['HTTP/1.1 -123']],
69 | [['']],
70 | [[]]
71 | ];
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Unit/HttpAdapter/WithoutAuthenticationCredentialsTest.php:
--------------------------------------------------------------------------------
1 | authenticate($expectedRequest);
27 |
28 | $this->assertSame($expectedRequest, $request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Unit/NchanTest.php:
--------------------------------------------------------------------------------
1 | createNchan()->channel('/my-channel');
21 |
22 | $this->assertInstanceOf(Channel::class, $channel);
23 | }
24 |
25 | /**
26 | * @test
27 | */
28 | public function itShouldCreateStatusApi(): void
29 | {
30 | $status = $this->createNchan()->status('/status');
31 |
32 | $this->assertInstanceOf(Status::class, $status);
33 | }
34 |
35 | /**
36 | * Returns a Nchan instance.
37 | *
38 | * @return Nchan
39 | */
40 | private function createNchan(): Nchan
41 | {
42 | $client = $this->createMock(Client::class);
43 |
44 | $nchan = new Nchan(
45 | 'http://localhost',
46 | $client
47 | );
48 |
49 | return $nchan;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------