Skip to content

Commit 8a75490

Browse files
committed
(test): Add tests for adapter factory callback and onResolve dynamic resolver
1 parent d3ff772 commit 8a75490

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed

tests/AdapterFactoryTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace Utopia\Tests;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Utopia\Proxy\Server\TCP\Config;
7+
8+
class AdapterFactoryTest extends TestCase
9+
{
10+
protected function setUp(): void
11+
{
12+
if (!\extension_loaded('swoole')) {
13+
$this->markTestSkipped('ext-swoole is required to run Config tests.');
14+
}
15+
}
16+
17+
public function testDefaultAdapterFactoryIsNull(): void
18+
{
19+
$config = new Config();
20+
$this->assertNull($config->adapterFactory);
21+
}
22+
23+
public function testAdapterFactoryAcceptsClosure(): void
24+
{
25+
$factory = function (int $port) {
26+
return 'adapter-for-port-' . $port;
27+
};
28+
29+
$config = new Config(adapterFactory: $factory);
30+
$this->assertNotNull($config->adapterFactory);
31+
$this->assertInstanceOf(\Closure::class, $config->adapterFactory);
32+
}
33+
34+
public function testAdapterFactoryClosureIsInvokable(): void
35+
{
36+
$factory = function (int $port): string {
37+
return 'adapter-for-port-' . $port;
38+
};
39+
40+
$config = new Config(adapterFactory: $factory);
41+
$result = ($config->adapterFactory)(5432);
42+
$this->assertSame('adapter-for-port-5432', $result);
43+
}
44+
45+
public function testAdapterFactoryClosureReceivesPort(): void
46+
{
47+
$receivedPorts = [];
48+
$factory = function (int $port) use (&$receivedPorts): string {
49+
$receivedPorts[] = $port;
50+
return 'adapter';
51+
};
52+
53+
$config = new Config(adapterFactory: $factory);
54+
($config->adapterFactory)(5432);
55+
($config->adapterFactory)(3306);
56+
($config->adapterFactory)(27017);
57+
58+
$this->assertSame([5432, 3306, 27017], $receivedPorts);
59+
}
60+
61+
public function testOtherConfigValuesPreservedWithFactory(): void
62+
{
63+
$factory = function (int $port) {
64+
return 'adapter';
65+
};
66+
67+
$config = new Config(
68+
host: '127.0.0.1',
69+
ports: [5432],
70+
workers: 8,
71+
adapterFactory: $factory,
72+
);
73+
74+
$this->assertSame('127.0.0.1', $config->host);
75+
$this->assertSame([5432], $config->ports);
76+
$this->assertSame(8, $config->workers);
77+
$this->assertNotNull($config->adapterFactory);
78+
}
79+
80+
public function testNullAdapterFactoryPreservesDefaults(): void
81+
{
82+
$config = new Config(adapterFactory: null);
83+
$this->assertNull($config->adapterFactory);
84+
$this->assertSame('0.0.0.0', $config->host);
85+
$this->assertSame([5432, 3306, 27017], $config->ports);
86+
}
87+
}

tests/OnResolveCallbackTest.php

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
namespace Utopia\Tests;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Utopia\Proxy\Adapter;
7+
use Utopia\Proxy\ConnectionResult;
8+
use Utopia\Proxy\Protocol;
9+
use Utopia\Proxy\Resolver\Exception as ResolverException;
10+
use Utopia\Proxy\Resolver\Result as ResolverResult;
11+
12+
class OnResolveCallbackTest extends TestCase
13+
{
14+
protected MockResolver $resolver;
15+
16+
protected function setUp(): void
17+
{
18+
if (!\extension_loaded('swoole')) {
19+
$this->markTestSkipped('ext-swoole is required to run adapter tests.');
20+
}
21+
22+
$this->resolver = new MockResolver();
23+
}
24+
25+
/**
26+
* Test that onResolve() sets the callback and returns the adapter for chaining
27+
*/
28+
public function testOnResolveSetsCallback(): void
29+
{
30+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
31+
32+
$result = $adapter->onResolve(function (string $resourceId) {
33+
return '1.2.3.4:8080';
34+
});
35+
36+
$this->assertSame($adapter, $result);
37+
}
38+
39+
/**
40+
* Test that route() uses the callback when set, bypassing the resolver
41+
*/
42+
public function testRouteUsesCallbackWhenSet(): void
43+
{
44+
$this->resolver->setEndpoint('should-not-be-used.example.com:8080');
45+
46+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
47+
$adapter->setSkipValidation(true);
48+
$adapter->onResolve(function (string $resourceId): string {
49+
return 'callback-host.example.com:9090';
50+
});
51+
52+
$result = $adapter->route('test-resource');
53+
54+
$this->assertInstanceOf(ConnectionResult::class, $result);
55+
$this->assertSame('callback-host.example.com:9090', $result->endpoint);
56+
}
57+
58+
/**
59+
* Test that route() falls back to resolver when callback is null
60+
*/
61+
public function testRouteFallsBackToResolverWhenCallbackIsNull(): void
62+
{
63+
$this->resolver->setEndpoint('resolver-host.example.com:8080');
64+
65+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
66+
$adapter->setSkipValidation(true);
67+
68+
$result = $adapter->route('test-resource');
69+
70+
$this->assertSame('resolver-host.example.com:8080', $result->endpoint);
71+
}
72+
73+
/**
74+
* Test that callback can return a string endpoint
75+
*/
76+
public function testCallbackReturnsStringEndpoint(): void
77+
{
78+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
79+
$adapter->setSkipValidation(true);
80+
$adapter->onResolve(function (string $resourceId): string {
81+
return 'string-endpoint.example.com:5432';
82+
});
83+
84+
$result = $adapter->route('my-db');
85+
86+
$this->assertSame('string-endpoint.example.com:5432', $result->endpoint);
87+
$this->assertFalse($result->metadata['cached']);
88+
}
89+
90+
/**
91+
* Test that callback can return a Result object
92+
*/
93+
public function testCallbackReturnsResultObject(): void
94+
{
95+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
96+
$adapter->setSkipValidation(true);
97+
$adapter->onResolve(function (string $resourceId): ResolverResult {
98+
return new ResolverResult(
99+
endpoint: 'result-endpoint.example.com:3306',
100+
metadata: ['custom' => 'metadata', 'resourceId' => $resourceId],
101+
);
102+
});
103+
104+
$result = $adapter->route('my-db');
105+
106+
$this->assertSame('result-endpoint.example.com:3306', $result->endpoint);
107+
$this->assertSame('metadata', $result->metadata['custom']);
108+
$this->assertSame('my-db', $result->metadata['resourceId']);
109+
$this->assertFalse($result->metadata['cached']);
110+
}
111+
112+
/**
113+
* Test that callback receives the correct resource ID
114+
*/
115+
public function testCallbackReceivesResourceId(): void
116+
{
117+
$receivedIds = [];
118+
119+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
120+
$adapter->setSkipValidation(true);
121+
$adapter->onResolve(function (string $resourceId) use (&$receivedIds): string {
122+
$receivedIds[] = $resourceId;
123+
return 'host.example.com:8080';
124+
});
125+
126+
$adapter->route('resource-alpha');
127+
// Wait for cache to expire
128+
$start = time();
129+
while (time() === $start) {
130+
usleep(1000);
131+
}
132+
$adapter->route('resource-beta');
133+
134+
$this->assertContains('resource-alpha', $receivedIds);
135+
$this->assertContains('resource-beta', $receivedIds);
136+
}
137+
138+
/**
139+
* Test that route() throws when neither callback nor resolver is set
140+
*/
141+
public function testRouteThrowsWhenNoCallbackOrResolver(): void
142+
{
143+
$adapter = new Adapter(null, name: 'Test', protocol: Protocol::HTTP);
144+
$adapter->setSkipValidation(true);
145+
146+
$this->expectException(ResolverException::class);
147+
$this->expectExceptionMessage('No resolver or resolve callback configured');
148+
149+
$adapter->route('test-resource');
150+
}
151+
152+
/**
153+
* Test that callback takes priority over resolver
154+
*/
155+
public function testCallbackTakesPriorityOverResolver(): void
156+
{
157+
$resolverCalled = false;
158+
159+
$mockResolver = new class ($resolverCalled) extends MockResolver {
160+
private bool $called;
161+
162+
public function __construct(bool &$called)
163+
{
164+
$this->called = &$called;
165+
parent::setEndpoint('resolver.example.com:8080');
166+
}
167+
168+
public function resolve(string $resourceId): \Utopia\Proxy\Resolver\Result
169+
{
170+
$this->called = true;
171+
return parent::resolve($resourceId);
172+
}
173+
};
174+
175+
$adapter = new Adapter($mockResolver, name: 'Test', protocol: Protocol::HTTP);
176+
$adapter->setSkipValidation(true);
177+
$adapter->onResolve(function (string $resourceId): string {
178+
return 'callback.example.com:8080';
179+
});
180+
181+
$result = $adapter->route('test-resource');
182+
183+
$this->assertSame('callback.example.com:8080', $result->endpoint);
184+
$this->assertFalse($resolverCalled);
185+
}
186+
187+
/**
188+
* Test that result from callback with string gets wrapped in default metadata
189+
*/
190+
public function testStringCallbackResultHasDefaultMetadata(): void
191+
{
192+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
193+
$adapter->setSkipValidation(true);
194+
$adapter->onResolve(function (string $resourceId): string {
195+
return 'host.example.com:8080';
196+
});
197+
198+
$result = $adapter->route('test-resource');
199+
200+
$this->assertArrayHasKey('cached', $result->metadata);
201+
$this->assertFalse($result->metadata['cached']);
202+
}
203+
204+
/**
205+
* Test that Result metadata from callback is merged into connection result
206+
*/
207+
public function testResultObjectMetadataIsMerged(): void
208+
{
209+
$adapter = new Adapter($this->resolver, name: 'Test', protocol: Protocol::HTTP);
210+
$adapter->setSkipValidation(true);
211+
$adapter->onResolve(function (string $resourceId): ResolverResult {
212+
return new ResolverResult(
213+
endpoint: 'host.example.com:8080',
214+
metadata: ['region' => 'us-east-1', 'tier' => 'premium'],
215+
);
216+
});
217+
218+
$result = $adapter->route('test-resource');
219+
220+
$this->assertSame('us-east-1', $result->metadata['region']);
221+
$this->assertSame('premium', $result->metadata['tier']);
222+
$this->assertFalse($result->metadata['cached']);
223+
}
224+
}

0 commit comments

Comments
 (0)