Skip to content

Commit 65a295a

Browse files
authored
Merge pull request #798 from tienvx/add-server-sent-events-example
test: Add server sent events example
2 parents 01043c9 + 1e37839 commit 65a295a

16 files changed

Lines changed: 520 additions & 4 deletions

File tree

.github/workflows/build.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ jobs:
9595
path: ~/.pact/plugins
9696
key: ${{ matrix.os }}-pact-plugins
9797

98+
- uses: pact-foundation/pact-cli@main
99+
100+
- name: Install SSE plugin
101+
run: pact plugin install https://github.com/tienvx/pact-sse-plugin/releases/tag/0.1.0 -y
102+
98103
- name: Run examples
99104
run: ${{ matrix.os == 'windows-latest' && 'composer run-examples:windows' || 'composer run-examples' }}
100105
env:
@@ -161,4 +166,4 @@ jobs:
161166
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
162167
name: Checkout repository
163168
- name: run musl # If shared-mime-info not installed - Expected binary contents to have content type 'image/jpeg' but detected contents was 'application/octet-stream'
164-
run: docker run --env PACT_DO_NOT_TRACK=true --rm -v $PWD:/home alpine:3.19 /bin/sh -c 'apk add --no-cache shared-mime-info php82-dev php82-ffi php82-pecl-grpc php82-sockets php82-tokenizer php82-dom php82-xml php82-xmlwriter php82-simplexml composer protobuf-dev && cd /home && composer install && composer test && composer run-examples'
169+
run: docker run --env PACT_DO_NOT_TRACK=true --rm -v $PWD:/home alpine:3.19 /bin/sh -c 'apk add --no-cache shared-mime-info php82-dev php82-ffi php82-pecl-grpc php82-sockets php82-tokenizer php82-dom php82-xml php82-xmlwriter php82-simplexml composer protobuf-dev curl file && cd /home && composer install && curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-cli/main/install.sh | sh && export PATH=/home:${PATH} && pact plugin install https://github.com/tienvx/pact-sse-plugin/releases/tag/0.1.0 -y && composer test && composer run-examples'

composer.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@
4141
"rector/rector": "^2.0",
4242
"clue/framework-x": "^0.17.0",
4343
"grpc/grpc": "^1.57",
44-
"google/protobuf": "^5.0"
44+
"google/protobuf": "^5.0",
45+
"hosmelq/sse": "^0.1.0"
4546
},
47+
"repositories": [
48+
{
49+
"type": "vcs",
50+
"url": "https://github.com/xadammr/sse-client"
51+
}
52+
],
4653
"suggest": {
4754
"ext-grpc": "If you want to use gRPC then you must install and use this"
4855
},
@@ -84,7 +91,8 @@
8491
"composer run-example:stub-server",
8592
"composer run-example:xml",
8693
"composer run-example:graphql",
87-
"composer run-example:form-urlencoded"
94+
"composer run-example:form-urlencoded",
95+
"composer run-example:server-sent-events"
8896
],
8997
"run-examples:windows": [
9098
"composer run-example:binary:windows",
@@ -99,7 +107,8 @@
99107
"composer run-example:stub-server:windows",
100108
"composer run-example:xml:windows",
101109
"composer run-example:graphql:windows",
102-
"composer run-example:form-urlencoded:windows"
110+
"composer run-example:form-urlencoded:windows",
111+
"composer run-example:server-sent-events:windows"
103112
],
104113
"run-example:binary:windows": [
105114
"powershell -Command \"if (Test-Path 'example/binary/pacts/binaryConsumer-binaryProvider.json') { Remove-Item 'example/binary/pacts/binaryConsumer-binaryProvider.json' }\"",
@@ -226,6 +235,16 @@
226235
"rm -f example/graphql/pacts/formUrlEncodedConsumer-formUrlEncodedProvider.json",
227236
"cd example/form-urlencoded/consumer && phpunit",
228237
"cd example/form-urlencoded/provider && phpunit"
238+
],
239+
"run-example:server-sent-events:windows": [
240+
"powershell -Command \"if (Test-Path 'example/server-sent-events/pacts/sseConsumer-sseProvider.json') { Remove-Item 'example/server-sent-events/pacts/sseConsumer-sseProvider.json' }\"",
241+
"cd example/server-sent-events/consumer && phpunit --process-isolation",
242+
"cd example/server-sent-events/provider && phpunit --process-isolation"
243+
],
244+
"run-example:server-sent-events": [
245+
"rm -f example/server-sent-events/pacts/sseConsumer-sseProvider.json",
246+
"cd example/server-sent-events/consumer && phpunit",
247+
"cd example/server-sent-events/provider && phpunit"
229248
]
230249
},
231250
"extra": {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
$loader = require __DIR__ . '/../../../vendor/autoload.php';
4+
$loader->addPsr4('ServerSentEventsConsumer\\', __DIR__ . '/src');
5+
$loader->addPsr4('ServerSentEventsConsumer\\Tests\\', __DIR__ . '/tests');
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit bootstrap="./autoload.php" colors="true">
3+
<testsuites>
4+
<testsuite name="PhpPact Example Tests">
5+
<directory>./tests</directory>
6+
</testsuite>
7+
</testsuites>
8+
<php>
9+
<env name="PACT_LOGLEVEL" value="DEBUG"/>
10+
</php>
11+
</phpunit>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace ServerSentEventsConsumer\Service;
4+
5+
use GuzzleHttp\Client;
6+
use GuzzleHttp\Psr7\Uri;
7+
use Psr\Http\Message\StreamInterface;
8+
9+
class HttpClientService
10+
{
11+
private Client $httpClient;
12+
13+
public function __construct(private string $baseUri)
14+
{
15+
$this->httpClient = new Client();
16+
}
17+
18+
public function getEvents(): StreamInterface
19+
{
20+
$response = $this->httpClient->get(new Uri("{$this->baseUri}/events"), [
21+
'headers' => [
22+
'Accept' => 'text/event-stream',
23+
'Cache-Control' => 'no-cache',
24+
'Last-Event-ID' => 123,
25+
],
26+
'stream' => true,
27+
'http_errors' => false,
28+
]);
29+
30+
return $response->getBody();
31+
}
32+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace ServerSentEventsConsumer\Tests\Service;
4+
5+
use PhpPact\Consumer\Driver\Enum\InteractionPart;
6+
use PhpPact\Consumer\InteractionBuilder;
7+
use PhpPact\Consumer\Matcher\Matcher;
8+
use PhpPact\Consumer\Model\Body\Text;
9+
use PhpPact\Consumer\Model\ConsumerRequest;
10+
use PhpPact\Consumer\Model\ProviderResponse;
11+
use PhpPact\Plugins\Sse\Factory\SseInteractionDriverFactory;
12+
use PhpPact\Standalone\MockService\MockServerConfig;
13+
use PHPUnit\Framework\TestCase;
14+
use HosmelQ\SSE\Client as SseClient;
15+
16+
class HttpClientServiceTest extends TestCase
17+
{
18+
public function testGetEvents(): void
19+
{
20+
$matcher = new Matcher(plugin: true);
21+
22+
$request = new ConsumerRequest();
23+
$request
24+
->setMethod('GET')
25+
->setPath('/events')
26+
->addHeader('Accept', 'text/event-stream')
27+
;
28+
29+
$response = new ProviderResponse();
30+
$response
31+
->setStatus(200)
32+
->setBody(new Text(
33+
json_encode([
34+
'id' => $matcher->number(100),
35+
// NOTE: Must use integerV3() instead of integer() because in SSE all values
36+
// are strings (e.g., 'data:100'). integer() returns a Type matcher which only
37+
// checks that both values are the same type (string), so 'aaa' would match '100'.
38+
// integerV3() returns an Integer matcher that properly validates the value is numeric.
39+
'retry' => $matcher->integerV3(100),
40+
'event' => $matcher->like('count'),
41+
'data' => $matcher->like("simple text\nother text"),
42+
'data[count]' => $matcher->number(100),
43+
'data[time]' => $matcher->datetime('yyyy-MM-dd', '2000-01-01'),
44+
'data[user]' => $matcher->term('id: 123, name: Bob', 'id: \d+, name: \w+'),
45+
]),
46+
'text/event-stream'
47+
))
48+
->setHeaders([
49+
'Cache-Control' => 'no-cache',
50+
'Connection' => 'keep-alive',
51+
'X-Accel-Buffering' => 'no',
52+
])
53+
;
54+
55+
$config = new MockServerConfig();
56+
$config
57+
->setConsumer('sseConsumer')
58+
->setProvider('sseProvider')
59+
->setPactSpecificationVersion('4.0.0')
60+
->setPactDir(__DIR__.'/../../../pacts');
61+
if ($logLevel = \getenv('PACT_LOGLEVEL')) {
62+
$config->setLogLevel($logLevel);
63+
}
64+
$builder = new InteractionBuilder($config, new SseInteractionDriverFactory(InteractionPart::RESPONSE));
65+
$builder
66+
->given('events are available')
67+
->uponReceiving('request for events')
68+
->with($request)
69+
->willRespondWith($response)
70+
;
71+
72+
$sseClient = new SseClient();
73+
$eventSource = $sseClient->get($config->getBaseUri() . '/events');
74+
$events = iterator_to_array($eventSource->events());
75+
76+
$expectedEvents = [
77+
['data' => "simple text\nother text", 'id' => '100', 'retry' => 100],
78+
['data' => '100', 'id' => '100', 'retry' => null],
79+
['data' => '2000-01-01', 'id' => '100', 'retry' => null],
80+
['data' => 'id: 123, name: Bob', 'id' => '100', 'retry' => null],
81+
];
82+
83+
$this->assertCount(count($expectedEvents), $events);
84+
foreach ($events as $index => $event) {
85+
$this->assertSame($expectedEvents[$index]['data'], $event->data);
86+
$this->assertSame($expectedEvents[$index]['id'], $event->id);
87+
$this->assertSame($expectedEvents[$index]['retry'], $event->retry);
88+
}
89+
90+
$this->assertTrue($builder->verify());
91+
}
92+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
{
2+
"consumer": {
3+
"name": "sseConsumer"
4+
},
5+
"interactions": [
6+
{
7+
"description": "request for events",
8+
"interactionMarkup": {
9+
"markup": "# SSE Events\n\n|type|data|\n|---|---|\n||simple text\nother text|\n|count|100|\n|time|2000-01-01|\n|user|id: 123, name: Bob|\n",
10+
"markupType": "COMMON_MARK"
11+
},
12+
"pending": false,
13+
"providerStates": [
14+
{
15+
"name": "events are available"
16+
}
17+
],
18+
"request": {
19+
"headers": {
20+
"Accept": [
21+
"text/event-stream"
22+
]
23+
},
24+
"method": "GET",
25+
"path": "/events"
26+
},
27+
"response": {
28+
"body": {
29+
"content": "retry:100\nid:100\ndata:simple text\ndata:other text\n\nevent:count\ndata:100\n\nevent:time\ndata:2000-01-01\n\nevent:user\ndata:id: 123, name: Bob\n\n",
30+
"contentType": "text/event-stream",
31+
"contentTypeHint": "DEFAULT",
32+
"encoded": false
33+
},
34+
"generators": {
35+
"body": {
36+
"data.time.*": {
37+
"format": "yyyy-MM-dd",
38+
"type": "DateTime"
39+
}
40+
}
41+
},
42+
"headers": {
43+
"Cache-Control": [
44+
"no-cache"
45+
],
46+
"Connection": [
47+
"keep-alive"
48+
],
49+
"X-Accel-Buffering": [
50+
"no"
51+
],
52+
"content-type": [
53+
"text/event-stream"
54+
]
55+
},
56+
"matchingRules": {
57+
"body": {
58+
"data.*": {
59+
"combine": "AND",
60+
"matchers": [
61+
{
62+
"match": "type"
63+
}
64+
]
65+
},
66+
"data.count.*": {
67+
"combine": "AND",
68+
"matchers": [
69+
{
70+
"match": "number"
71+
}
72+
]
73+
},
74+
"data.time.*": {
75+
"combine": "AND",
76+
"matchers": [
77+
{
78+
"format": "yyyy-MM-dd",
79+
"match": "datetime"
80+
}
81+
]
82+
},
83+
"data.user.*": {
84+
"combine": "AND",
85+
"matchers": [
86+
{
87+
"match": "regex",
88+
"regex": "id: \\d+, name: \\w+"
89+
}
90+
]
91+
},
92+
"event": {
93+
"combine": "AND",
94+
"matchers": [
95+
{
96+
"match": "type"
97+
}
98+
]
99+
},
100+
"id.*": {
101+
"combine": "AND",
102+
"matchers": [
103+
{
104+
"match": "number"
105+
}
106+
]
107+
},
108+
"retry": {
109+
"combine": "AND",
110+
"matchers": [
111+
{
112+
"match": "integer"
113+
}
114+
]
115+
}
116+
}
117+
},
118+
"status": 200
119+
},
120+
"transport": "http",
121+
"type": "Synchronous/HTTP"
122+
}
123+
],
124+
"metadata": {
125+
"pactRust": {
126+
"ffi": "0.5.3",
127+
"mockserver": "2.2.1",
128+
"models": "1.3.9"
129+
},
130+
"pactSpecification": {
131+
"version": "4.0"
132+
},
133+
"plugins": [
134+
{
135+
"configuration": {},
136+
"name": "sse",
137+
"version": "0.1.0"
138+
}
139+
]
140+
},
141+
"provider": {
142+
"name": "sseProvider"
143+
}
144+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
$loader = require __DIR__ . '/../../../vendor/autoload.php';
4+
$loader->addPsr4('ServerSentEventsProvider\\Tests\\', __DIR__ . '/tests');
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit bootstrap="./autoload.php" colors="true">
3+
<testsuites>
4+
<testsuite name="PhpPact Example Tests">
5+
<directory>./tests</directory>
6+
</testsuite>
7+
</testsuites>
8+
<php>
9+
<env name="PACT_LOGLEVEL" value="DEBUG"/>
10+
</php>
11+
</phpunit>

0 commit comments

Comments
 (0)