Skip to content

Commit c02cb8e

Browse files
committed
Add test coverage
1 parent ed16c93 commit c02cb8e

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
<?php
2+
3+
use PsrDiscovery\Implementations\Psr18\Clients;
4+
use PsrDiscovery\Collections\CandidatesCollection;
5+
use PsrDiscovery\Entities\CandidateEntity;
6+
use Psr\Http\Client\ClientInterface;
7+
use Psr\Http\Message\RequestInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
10+
beforeEach(function () {
11+
// Reset state before each test.
12+
Clients::use(null);
13+
14+
// Use reflection to set private static properties.
15+
$reflection = new ReflectionClass(Clients::class);
16+
17+
// Use a package name that is installed to satisfy Composer's package check.
18+
$mockPackageName = 'psr/http-client';
19+
$mockPackageVersion = '^1.0';
20+
21+
// Set 'candidates' and 'extendedCandidates' to a custom collection with safe candidates.
22+
$customCandidates = CandidatesCollection::create([
23+
$mockPackageName => CandidateEntity::create(
24+
package: $mockPackageName,
25+
version: $mockPackageVersion,
26+
builder: static fn () => new class implements ClientInterface {
27+
public function sendRequest(RequestInterface $request): ResponseInterface
28+
{
29+
// Return a mock response.
30+
return new class implements ResponseInterface {
31+
// Implement ResponseInterface methods.
32+
public function getProtocolVersion() { return '1.1'; }
33+
public function withProtocolVersion($version) { return $this; }
34+
public function getHeaders() { return []; }
35+
public function hasHeader($name) { return false; }
36+
public function getHeader($name) { return []; }
37+
public function getHeaderLine($name) { return ''; }
38+
public function withHeader($name, $value) { return $this; }
39+
public function withAddedHeader($name, $value) { return $this; }
40+
public function withoutHeader($name) { return $this; }
41+
public function getBody() { return new class implements \Psr\Http\Message\StreamInterface {
42+
public function __toString() { return ''; }
43+
public function close() {}
44+
public function detach() {}
45+
public function getSize() { return null; }
46+
public function tell() { return 0; }
47+
public function eof() { return true; }
48+
public function isSeekable() { return false; }
49+
public function seek($offset, $whence = SEEK_SET) {}
50+
public function rewind() {}
51+
public function isWritable() { return false; }
52+
public function write($string) { return 0; }
53+
public function isReadable() { return false; }
54+
public function read($length) { return ''; }
55+
public function getContents() { return ''; }
56+
public function getMetadata($key = null) { return null; }
57+
}; }
58+
public function withBody(\Psr\Http\Message\StreamInterface $body) { return $this; }
59+
public function getStatusCode() { return 200; }
60+
public function withStatus($code, $reasonPhrase = '') { return $this; }
61+
public function getReasonPhrase() { return 'OK'; }
62+
};
63+
}
64+
},
65+
),
66+
]);
67+
68+
// Set 'candidates' and 'extendedCandidates' to the custom collection.
69+
foreach (['candidates', 'extendedCandidates'] as $property) {
70+
$prop = $reflection->getProperty($property);
71+
$prop->setAccessible(true);
72+
$prop->setValue(null, $customCandidates);
73+
}
74+
75+
// Reset 'singleton' and 'using' to null.
76+
foreach (['singleton', 'using'] as $property) {
77+
$prop = $reflection->getProperty($property);
78+
$prop->setAccessible(true);
79+
$prop->setValue(null, null);
80+
}
81+
});
82+
83+
it('discovers a client implementation from custom candidates', function () {
84+
// Call the discover method.
85+
$client = Clients::discover();
86+
87+
// Assert that the client is our mock client.
88+
expect($client)->toBeInstanceOf(ClientInterface::class);
89+
});
90+
91+
it('retrieves all discovered client implementations', function () {
92+
$candidates = Clients::discoveries();
93+
94+
expect($candidates)->toBeArray();
95+
expect(count($candidates))->toBeGreaterThan(0);
96+
97+
// Build each candidate to get the client instance.
98+
foreach ($candidates as $candidate) {
99+
$client = $candidate->build();
100+
expect($client)->toBeInstanceOf(ClientInterface::class);
101+
}
102+
});
103+
104+
it('returns the singleton client or discovered one if none is set', function () {
105+
// Case 1: No client set, expect singleton() to return the discovered client.
106+
$client = Clients::singleton();
107+
expect($client)->toBeInstanceOf(ClientInterface::class);
108+
109+
// Case 2: Set a mock client and verify it's returned.
110+
/** @var ClientInterface $mockClient */
111+
$mockClient = mock(ClientInterface::class);
112+
Clients::use($mockClient);
113+
114+
expect(Clients::singleton())->toBe($mockClient);
115+
});
116+
117+
it('adds a candidate and resets the singleton client', function () {
118+
// Ensure the singleton client is set.
119+
/** @var ClientInterface $mockClient */
120+
$mockClient = mock(ClientInterface::class);
121+
Clients::use($mockClient);
122+
expect(Clients::singleton())->toBe($mockClient);
123+
124+
// Add a candidate with an installed package name.
125+
$candidate = CandidateEntity::create(
126+
package: 'psr/http-client',
127+
version: '^1.0',
128+
builder: static fn () => mock(ClientInterface::class),
129+
);
130+
Clients::add($candidate);
131+
132+
// Verify the candidate was added.
133+
$candidates = Clients::candidates();
134+
expect($candidates->has('psr/http-client'))->toBeTrue();
135+
expect($candidates->get('psr/http-client'))->toBe($candidate);
136+
137+
// Verify the singleton client has been reset and a new client is discovered.
138+
$singletonClient = Clients::singleton();
139+
expect($singletonClient)->toBeInstanceOf(ClientInterface::class);
140+
expect($singletonClient)->not->toBe($mockClient);
141+
});
142+
143+
it('sets and clears the singleton client using use()', function () {
144+
/** @var ClientInterface $mockClient */
145+
$mockClient = mock(ClientInterface::class);
146+
147+
// Set a specific client.
148+
Clients::use($mockClient);
149+
expect(Clients::singleton())->toBe($mockClient);
150+
151+
// Clear the current client.
152+
Clients::use(null);
153+
// After clearing, singleton should return the discovered client.
154+
$singletonClient = Clients::singleton();
155+
expect($singletonClient)->toBeInstanceOf(ClientInterface::class);
156+
expect($singletonClient)->not->toBe($mockClient);
157+
});
158+
159+
it('adds and clears candidates', function () {
160+
$collection = CandidatesCollection::create();
161+
$candidate = CandidateEntity::create(
162+
package: 'psr/http-client',
163+
version: '^1.0',
164+
builder: static fn () => mock(ClientInterface::class),
165+
);
166+
167+
Clients::set($collection);
168+
expect(Clients::candidates()->all())->toBeEmpty();
169+
170+
Clients::add($candidate);
171+
expect(Clients::candidates()->all())->toHaveKey('psr/http-client');
172+
});
173+
174+
it('prioritizes a preferred candidate', function () {
175+
$candidate1 = CandidateEntity::create(
176+
package: 'package/one',
177+
version: '^1.0',
178+
builder: static fn () => mock(ClientInterface::class),
179+
);
180+
181+
$candidate2 = CandidateEntity::create(
182+
package: 'package/two',
183+
version: '^1.0',
184+
builder: static fn () => mock(ClientInterface::class),
185+
);
186+
187+
$collection = CandidatesCollection::create([
188+
'package/one' => $candidate1,
189+
'package/two' => $candidate2,
190+
]);
191+
192+
// Use reflection to set 'candidates' and 'extendedCandidates' to our custom collection.
193+
$reflection = new ReflectionClass(Clients::class);
194+
195+
foreach (['candidates', 'extendedCandidates'] as $property) {
196+
$prop = $reflection->getProperty($property);
197+
$prop->setAccessible(true);
198+
$prop->setValue(null, $collection);
199+
}
200+
201+
Clients::prefer('package/two');
202+
203+
$candidates = array_keys(Clients::candidates()->all());
204+
expect($candidates[0])->toBe('package/two');
205+
});
206+
207+
it('uses a custom CandidatesCollection for allCandidates', function () {
208+
// Create a custom collection.
209+
$customCollection = CandidatesCollection::create([
210+
'psr/http-client' => CandidateEntity::create(
211+
package: 'psr/http-client',
212+
version: '^1.0',
213+
builder: static fn () => mock(ClientInterface::class),
214+
),
215+
]);
216+
217+
// Use reflection to set both 'candidates' and 'extendedCandidates' to the custom collection.
218+
$reflection = new ReflectionClass(Clients::class);
219+
220+
foreach (['candidates', 'extendedCandidates'] as $property) {
221+
$prop = $reflection->getProperty($property);
222+
$prop->setAccessible(true);
223+
$prop->setValue(null, $customCollection);
224+
}
225+
226+
// Verify allCandidates returns the custom collection.
227+
$allCandidates = Clients::allCandidates();
228+
expect($allCandidates->all())->toHaveKey('psr/http-client');
229+
expect($allCandidates->get('psr/http-client'))->toBeInstanceOf(CandidateEntity::class);
230+
});
231+
232+
it('returns a CandidatesCollection instance from candidates()', function () {
233+
$candidates = Clients::candidates();
234+
expect($candidates)->toBeInstanceOf(CandidatesCollection::class);
235+
});
236+
237+
it('returns the same CandidatesCollection instance upon multiple calls', function () {
238+
$firstCall = Clients::candidates();
239+
$secondCall = Clients::candidates();
240+
expect($firstCall)->toBe($secondCall);
241+
});
242+
243+
it('initializes default candidates in candidates()', function () {
244+
// Reset the candidates to ensure fresh initialization.
245+
$reflection = new ReflectionClass(Clients::class);
246+
$prop = $reflection->getProperty('candidates');
247+
$prop->setAccessible(true);
248+
$prop->setValue(null, null);
249+
250+
$candidates = Clients::candidates();
251+
expect($candidates)->toBeInstanceOf(CandidatesCollection::class);
252+
expect($candidates->all())->not->toBeEmpty();
253+
});
254+
255+
it('returns a CandidatesCollection instance from allCandidates()', function () {
256+
$allCandidates = Clients::allCandidates();
257+
expect($allCandidates)->toBeInstanceOf(CandidatesCollection::class);
258+
});
259+
260+
it('returns the same CandidatesCollection instance upon multiple calls to allCandidates()', function () {
261+
$firstCall = Clients::allCandidates();
262+
$secondCall = Clients::allCandidates();
263+
expect($firstCall)->toBe($secondCall);
264+
});
265+
266+
it('initializes extended candidates in allCandidates()', function () {
267+
// Reset the extendedCandidates to ensure fresh initialization.
268+
$reflection = new ReflectionClass(Clients::class);
269+
$prop = $reflection->getProperty('extendedCandidates');
270+
$prop->setAccessible(true);
271+
$prop->setValue(null, null);
272+
273+
$allCandidates = Clients::allCandidates();
274+
expect($allCandidates)->toBeInstanceOf(CandidatesCollection::class);
275+
expect($allCandidates->all())->not->toBeEmpty();
276+
});
277+
278+
it('does not alter candidates when prefer() is called with a non-existent package', function () {
279+
// Attempt to prefer a package not in the candidates list.
280+
Clients::prefer('non-existent/package');
281+
282+
$candidates = Clients::candidates();
283+
expect($candidates->all())->not->toHaveKey('non-existent/package');
284+
});
285+
286+
it('reorders candidates when prefer() is called with an existing package', function () {
287+
// Ensure the candidates are initialized.
288+
Clients::candidates();
289+
290+
// Prefer an existing package.
291+
Clients::prefer('psr/http-client');
292+
293+
$candidates = array_keys(Clients::candidates()->all());
294+
expect($candidates[0])->toBe('psr/http-client');
295+
});
296+
297+
it('resets singleton and using clients when use() is called with null', function () {
298+
// Set up an initial singleton client.
299+
/** @var ClientInterface $initialClient */
300+
$initialClient = mock(ClientInterface::class);
301+
Clients::use($initialClient);
302+
expect(Clients::singleton())->toBe($initialClient);
303+
304+
// Use null to reset the singleton and using clients.
305+
Clients::use(null);
306+
307+
// Verify that the singleton client has been reset.
308+
$singletonClient = Clients::singleton();
309+
expect($singletonClient)->toBeInstanceOf(ClientInterface::class);
310+
expect($singletonClient)->not->toBe($initialClient);
311+
});
312+
313+
it('returns the client set by use() when discover() is called', function () {
314+
/** @var ClientInterface $mockClient */
315+
$mockClient = mock(ClientInterface::class);
316+
Clients::use($mockClient);
317+
318+
// Call discover and expect to get the client we set with use()
319+
$client = Clients::discover();
320+
expect($client)->toBe($mockClient);
321+
});

0 commit comments

Comments
 (0)