Skip to content

Commit 62e1fb3

Browse files
committed
Merge pull request #1 from overblog/fix_access_resolve_false_values
Fix Access resolving when field value is false or 0 or empty string
2 parents b9400f7 + c534593 commit 62e1fb3

File tree

3 files changed

+141
-29
lines changed

3 files changed

+141
-29
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ Expression | Description | Scope
707707
**request** | Refers to the current request. | Request
708708
**token** | Refers to the token which is currently in the security token storage. | Token
709709
**user** | Refers to the user which is currently in the security token storage. | Valid Token
710-
**object** | Refers to the object for which access is being requested. | only available for `config.fields.*.access`
710+
**object** | Refers to the value of the field for which access is being requested. For array `object` will be each item of the array. For Relay connection `object` will be the node of each connection edges. | only available for `config.fields.*.access`
711711
**value** | Resolver value | only available in resolve context
712712
**args** | Resolver args array | only available in resolve context
713713
**info** | Resolver GraphQL\Type\Definition\ResolveInfo Object | only available in resolve context

Resolver/ConfigResolver.php

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ private function resolveAccessAndWrapResolveCallback($expression, callable $reso
256256

257257
$values = call_user_func_array([$this, 'resolveResolveCallbackArgs'], $args);
258258

259-
$checkAccess = function ($object) use ($expression, $values) {
259+
$checkAccess = function ($object, $throwException = false) use ($expression, $values) {
260260
try {
261261
$access = $this->resolveUsingExpressionLanguageIfNeeded(
262262
$expression,
@@ -266,33 +266,39 @@ private function resolveAccessAndWrapResolveCallback($expression, callable $reso
266266
$access = false;
267267
}
268268

269-
if (!$access) {
269+
if ($throwException && !$access) {
270270
throw new UserError('Access denied to this field.');
271271
}
272272

273-
return true;
273+
return $access;
274274
};
275275

276-
if (is_array($result) || $result instanceof \ArrayAccess) {
277-
$result = array_filter(
278-
array_map(
279-
function ($object) use ($checkAccess) {
280-
return $checkAccess($object) ? $object : null;
276+
switch (true) {
277+
case is_array($result) || $result instanceof \ArrayAccess:
278+
$result = array_filter(
279+
array_map(
280+
function ($object) use ($checkAccess) {
281+
return $checkAccess($object) ? $object : null;
282+
},
283+
$result
284+
)
285+
);
286+
break;
287+
288+
case $result instanceof Connection:
289+
$result->edges = array_map(
290+
function (Edge $edge) use ($checkAccess) {
291+
$edge->node = $checkAccess($edge->node) ? $edge->node : null;
292+
293+
return $edge;
281294
},
282-
$result
283-
)
284-
);
285-
} elseif ($result instanceof Connection) {
286-
$result->edges = array_map(
287-
function (Edge $edge) use ($checkAccess) {
288-
$edge->node = $checkAccess($edge->node) ? $edge->node : null;
289-
290-
return $edge;
291-
},
292-
$result->edges
293-
);
294-
} elseif (!empty($result) && !$checkAccess($result)) {
295-
$result = null;
295+
$result->edges
296+
);
297+
break;
298+
299+
default:
300+
$checkAccess($result, true);
301+
break;
296302
}
297303

298304
return $result;

Tests/Resolver/ConfigResolverTest.php

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@
1212
namespace Overblog\GraphQLBundle\Tests\Resolver;
1313

1414
use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage;
15+
use Overblog\GraphQLBundle\Relay\Connection\Output\ConnectionBuilder;
1516
use Overblog\GraphQLBundle\Resolver\ConfigResolver;
17+
use Overblog\GraphQLBundle\Tests\DIContainerMockTrait;
18+
use Symfony\Component\ExpressionLanguage\Expression;
1619

1720
class ConfigResolverTest extends \PHPUnit_Framework_TestCase
1821
{
22+
use DIContainerMockTrait;
23+
1924
/** @var ConfigResolver */
20-
private static $configResolver;
25+
private $configResolver;
2126

2227
public function setUp()
2328
{
24-
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\Container')
25-
->getMock();
29+
$container = $this->getDIContainerMock();
2630
$container
2731
->method('get')
2832
->will($this->returnValue(new \stdClass()));
@@ -51,7 +55,7 @@ public function setUp()
5155
->method('resolve')
5256
->will($this->returnValue(new \stdClass()));
5357

54-
self::$configResolver = new ConfigResolver(
58+
$this->configResolver = new ConfigResolver(
5559
$typeResolver,
5660
$fieldResolver,
5761
$argResolver,
@@ -66,12 +70,12 @@ public function setUp()
6670
*/
6771
public function testConfigNotArrayOrImplementArrayAccess()
6872
{
69-
self::$configResolver->resolve('Not Array');
73+
$this->configResolver->resolve('Not Array');
7074
}
7175

7276
public function testResolveValues()
7377
{
74-
$config = self::$configResolver->resolve(
78+
$config = $this->configResolver->resolve(
7579
[
7680
'values' => [
7781
'test' => ['value' => 'my test value'],
@@ -91,4 +95,106 @@ public function testResolveValues()
9195

9296
$this->assertEquals($expected, $config);
9397
}
98+
99+
/**
100+
* @expectedException \Overblog\GraphQLBundle\Error\UserError
101+
* @expectedExceptionMessage Access denied to this field
102+
*/
103+
public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndAccessDenied()
104+
{
105+
$callback = $this->invokeResolveAccessAndWrapResolveCallback(false);
106+
$callback('toto');
107+
}
108+
109+
/**
110+
* @expectedException \Overblog\GraphQLBundle\Error\UserError
111+
* @expectedExceptionMessage Access denied to this field
112+
*/
113+
public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndExpressionEvalThrowingException()
114+
{
115+
$callback = $this->invokeResolveAccessAndWrapResolveCallback('@=oups');
116+
$callback('titi');
117+
}
118+
119+
public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndAccessDeniedGranted()
120+
{
121+
$callback = $this->invokeResolveAccessAndWrapResolveCallback(true);
122+
$this->assertEquals('toto', $callback('toto'));
123+
}
124+
125+
public function testResolveAccessAndWrapResolveCallbackWithArrayAndAccessDeniedToEveryItemStartingByTo()
126+
{
127+
$callback = $this->invokeResolveAccessAndWrapResolveCallback('@=not(object matches "/^to.*/i")');
128+
$this->assertEquals(
129+
[
130+
'tata',
131+
'titi',
132+
'tata',
133+
],
134+
$callback(
135+
[
136+
'tata',
137+
'titi',
138+
'tata',
139+
'toto',
140+
'tota',
141+
]
142+
)
143+
);
144+
}
145+
146+
public function testResolveAccessAndWrapResolveCallbackWithRelayConnectionAndAccessGrantedToEveryNodeStartingByTo()
147+
{
148+
$callback = $this->invokeResolveAccessAndWrapResolveCallback('@=object matches "/^to.*/i"');
149+
$this->assertEquals(
150+
ConnectionBuilder::connectionFromArray(['toto', 'toti', null, null, null]),
151+
$callback(
152+
ConnectionBuilder::connectionFromArray(['toto', 'toti', 'coco', 'foo', 'bar'])
153+
)
154+
);
155+
}
156+
157+
/**
158+
* @param bool|string $hasAccess
159+
* @param callable|null $callback
160+
*
161+
* @return callback
162+
*/
163+
private function invokeResolveAccessAndWrapResolveCallback($hasAccess, callable $callback = null)
164+
{
165+
if (null === $callback) {
166+
$callback = function ($value) {
167+
return $value;
168+
};
169+
}
170+
171+
return $this->invokeMethod(
172+
$this->configResolver,
173+
'resolveAccessAndWrapResolveCallback',
174+
[
175+
$hasAccess,
176+
$callback,
177+
]
178+
);
179+
}
180+
181+
/**
182+
* Call protected/private method of a class.
183+
*
184+
* @see https://jtreminio.com/2013/03/unit-testing-tutorial-part-3-testing-protected-private-methods-coverage-reports-and-crap/
185+
*
186+
* @param object $object Instantiated object that we will run method on.
187+
* @param string $methodName Method name to call
188+
* @param array $parameters Array of parameters to pass into method.
189+
*
190+
* @return mixed Method return.
191+
*/
192+
private function invokeMethod($object, $methodName, array $parameters = [])
193+
{
194+
$reflection = new \ReflectionClass(get_class($object));
195+
$method = $reflection->getMethod($methodName);
196+
$method->setAccessible(true);
197+
198+
return $method->invokeArgs($object, $parameters);
199+
}
94200
}

0 commit comments

Comments
 (0)