Skip to content

Commit 34afc14

Browse files
committed
phpunit and phpcs
1 parent 0b1276f commit 34afc14

File tree

6 files changed

+344
-10
lines changed

6 files changed

+344
-10
lines changed

phpunit.xml.dist

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
verbose="true">
1414
<testsuites>
1515
<testsuite name="Feature">
16-
<directory suffix="Test.php">./test/Feature</directory>
16+
<directory suffix="Test.php">./test</directory>
1717
</testsuite>
1818
</testsuites>
1919

src/ApiProblem.php

+6-7
Original file line numberDiff line numberDiff line change
@@ -219,21 +219,20 @@ public function toArray(): array
219219
* Compose a response and return it.
220220
* The first two parameters are reversed to match Laravel response() params
221221
*
222-
* @param string[] $additional
222+
* @param mixed[] $params
223223
*/
224-
public function response(...$params)
225-
// public function response(string|Throwable $detail, int|string $status, ?string $type = null, ?string $title = null, array $additional = []): JsonResponse
224+
public function response(array ...$params): JsonResponse
226225
{
227226
$apProblem = null;
228227

229228
if ($this->getStatus()) {
230229
// Use current object
231230
$apiProblem = $this;
232231
} else {
233-
$status = $params[1] ?? null;
234-
$detail = $params[0] ?? null;
235-
$type = $params[2] ?? null;
236-
$title = $params[3] ?? null;
232+
$status = $params[1] ?? null;
233+
$detail = $params[0] ?? null;
234+
$type = $params[2] ?? null;
235+
$title = $params[3] ?? null;
237236
$additional = $params[4] ?? [];
238237

239238
// Called from a Facade, use local object

src/Exception/DomainException.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletons\Laravel\ApiProblem\Exception;
6+
7+
use Traversable;
8+
9+
class DomainException extends \DomainException implements
10+
ExceptionInterface,
11+
ProblemExceptionInterface
12+
{
13+
protected ?string $type = null;
14+
15+
/** @var string[] */
16+
protected array $details = [];
17+
18+
protected ?string $title = null;
19+
20+
/**
21+
* @param string[] $details
22+
*/
23+
public function setAdditionalDetails(array $details): self
24+
{
25+
$this->details = $details;
26+
27+
return $this;
28+
}
29+
30+
public function setType(string $uri): self
31+
{
32+
$this->type = (string) $uri;
33+
34+
return $this;
35+
}
36+
37+
public function setTitle(string $title): self
38+
{
39+
$this->title = (string) $title;
40+
41+
return $this;
42+
}
43+
44+
public function getAdditionalDetails(): Traversable|array|null
45+
{
46+
return $this->details;
47+
}
48+
49+
public function getType(): ?string
50+
{
51+
return $this->type;
52+
}
53+
54+
public function getTitle(): ?string
55+
{
56+
return $this->title;
57+
}
58+
}

src/Exception/ProblemExceptionInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface ProblemExceptionInterface
1313
{
1414
public function getAdditionalDetails(): array|Traversable|null;
1515

16-
public function getType(): string;
16+
public function getType(): ?string;
1717

18-
public function getTitle(): string;
18+
public function getTitle(): ?string;
1919
}

test/ApiProblemTest.php

+258
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletonsTest\Laravel\ApiProblem;
6+
7+
use ApiSkeletons\Laravel\ApiProblem\ApiProblem;
8+
use ApiSkeletons\Laravel\ApiProblem\Exception;
9+
use ReflectionObject;
10+
use TypeError;
11+
12+
final class ApiProblemTest extends TestCase
13+
{
14+
/** @psalm-return array<string, array{0: int}> */
15+
public function statusCodes(): array
16+
{
17+
return [
18+
'200' => [200],
19+
'201' => [201],
20+
'300' => [300],
21+
'301' => [301],
22+
'302' => [302],
23+
'400' => [400],
24+
'401' => [401],
25+
'404' => [404],
26+
'500' => [500],
27+
];
28+
}
29+
30+
/**
31+
* @dataProvider statusCodes
32+
*/
33+
public function testStatusIsUsedVerbatim(int $status): void
34+
{
35+
$apiProblem = new ApiProblem($status, 'foo');
36+
$payload = $apiProblem->toArray();
37+
$this->assertArrayHasKey('status', $payload);
38+
$this->assertEquals($status, $payload['status']);
39+
}
40+
41+
/**
42+
* @requires PHP 7.0
43+
*/
44+
public function testErrorAsDetails(): void
45+
{
46+
$error = new TypeError('error message', 705);
47+
$apiProblem = new ApiProblem(500, $error);
48+
$payload = $apiProblem->toArray();
49+
50+
$this->assertArrayHasKey('title', $payload);
51+
$this->assertEquals('TypeError', $payload['title']);
52+
$this->assertArrayHasKey('status', $payload);
53+
$this->assertEquals(705, $payload['status']);
54+
$this->assertArrayHasKey('detail', $payload);
55+
$this->assertEquals('error message', $payload['detail']);
56+
}
57+
58+
public function testExceptionCodeIsUsedForStatus(): void
59+
{
60+
$exception = new \Exception('exception message', 401);
61+
$apiProblem = new ApiProblem('500', $exception);
62+
$payload = $apiProblem->toArray();
63+
$this->assertArrayHasKey('status', $payload);
64+
$this->assertEquals($exception->getCode(), $payload['status']);
65+
}
66+
67+
public function testDetailStringIsUsedVerbatim(): void
68+
{
69+
$apiProblem = new ApiProblem('500', 'foo');
70+
$payload = $apiProblem->toArray();
71+
$this->assertArrayHasKey('detail', $payload);
72+
$this->assertEquals('foo', $payload['detail']);
73+
}
74+
75+
public function testExceptionMessageIsUsedForDetail(): void
76+
{
77+
$exception = new \Exception('exception message');
78+
$apiProblem = new ApiProblem('500', $exception);
79+
$payload = $apiProblem->toArray();
80+
$this->assertArrayHasKey('detail', $payload);
81+
$this->assertEquals($exception->getMessage(), $payload['detail']);
82+
}
83+
84+
public function testExceptionsCanTriggerInclusionOfStackTraceInDetails(): void
85+
{
86+
$exception = new \Exception('exception message');
87+
$apiProblem = new ApiProblem('500', $exception);
88+
$apiProblem->setDetailIncludesStackTrace(true);
89+
$payload = $apiProblem->toArray();
90+
$this->assertArrayHasKey('trace', $payload);
91+
$this->assertIsArray($payload['trace']);
92+
$this->assertEquals($exception->getTrace(), $payload['trace']);
93+
}
94+
95+
public function testExceptionsCanTriggerInclusionOfNestedExceptions(): void
96+
{
97+
$exceptionChild = new \Exception('child exception');
98+
$exceptionParent = new \Exception('parent exception', 0, $exceptionChild);
99+
100+
$apiProblem = new ApiProblem('500', $exceptionParent);
101+
$apiProblem->setDetailIncludesStackTrace(true);
102+
$payload = $apiProblem->toArray();
103+
$this->assertArrayHasKey('exception_stack', $payload);
104+
$this->assertIsArray($payload['exception_stack']);
105+
$expected = [
106+
[
107+
'code' => $exceptionChild->getCode(),
108+
'message' => $exceptionChild->getMessage(),
109+
'trace' => $exceptionChild->getTrace(),
110+
],
111+
];
112+
$this->assertEquals($expected, $payload['exception_stack']);
113+
}
114+
115+
public function testTypeUrlIsUsedVerbatim(): void
116+
{
117+
$apiProblem = new ApiProblem('500', 'foo', 'http://status.dev:8080/details.md');
118+
$payload = $apiProblem->toArray();
119+
$this->assertArrayHasKey('type', $payload);
120+
$this->assertEquals('http://status.dev:8080/details.md', $payload['type']);
121+
}
122+
123+
/** @psalm-return array<string, array{0: int}> */
124+
public function knownStatusCodes(): array
125+
{
126+
return [
127+
'404' => [404],
128+
'409' => [409],
129+
'422' => [422],
130+
'500' => [500],
131+
];
132+
}
133+
134+
/**
135+
* @dataProvider knownStatusCodes
136+
*/
137+
public function testKnownStatusResultsInKnownTitle(int $status): void
138+
{
139+
$apiProblem = new ApiProblem($status, 'foo');
140+
$r = new ReflectionObject($apiProblem);
141+
$p = $r->getProperty('problemStatusTitles');
142+
$p->setAccessible(true);
143+
$titles = $p->getValue($apiProblem);
144+
145+
$payload = $apiProblem->toArray();
146+
$this->assertArrayHasKey('title', $payload);
147+
$this->assertEquals($titles[$status], $payload['title']);
148+
}
149+
150+
public function testUnknownStatusResultsInUnknownTitle(): void
151+
{
152+
$apiProblem = new ApiProblem(420, 'foo');
153+
$payload = $apiProblem->toArray();
154+
$this->assertArrayHasKey('title', $payload);
155+
$this->assertEquals('Unknown', $payload['title']);
156+
}
157+
158+
public function testProvidedTitleIsUsedVerbatim(): void
159+
{
160+
$apiProblem = new ApiProblem('500', 'foo', 'http://status.dev:8080/details.md', 'some title');
161+
$payload = $apiProblem->toArray();
162+
$this->assertArrayHasKey('title', $payload);
163+
$this->assertEquals('some title', $payload['title']);
164+
}
165+
166+
public function testCanPassArbitraryDetailsToConstructor(): void
167+
{
168+
$problem = new ApiProblem(
169+
400,
170+
'Invalid input',
171+
'http://example.com/api/problem/400',
172+
'Invalid entity',
173+
['foo' => 'bar']
174+
);
175+
$this->assertEquals('bar', $problem->foo);
176+
}
177+
178+
public function testArraySerializationIncludesArbitraryDetails(): void
179+
{
180+
$problem = new ApiProblem(
181+
400,
182+
'Invalid input',
183+
'http://example.com/api/problem/400',
184+
'Invalid entity',
185+
['foo' => 'bar']
186+
);
187+
$array = $problem->toArray();
188+
$this->assertArrayHasKey('foo', $array);
189+
$this->assertEquals('bar', $array['foo']);
190+
}
191+
192+
public function testArbitraryDetailsShouldNotOverwriteRequiredFieldsInArraySerialization(): void
193+
{
194+
$problem = new ApiProblem(
195+
400,
196+
'Invalid input',
197+
'http://example.com/api/problem/400',
198+
'Invalid entity',
199+
['title' => 'SHOULD NOT GET THIS']
200+
);
201+
$array = $problem->toArray();
202+
$this->assertArrayHasKey('title', $array);
203+
$this->assertEquals('Invalid entity', $array['title']);
204+
}
205+
206+
public function testUsesTitleFromExceptionWhenProvided(): void
207+
{
208+
$exception = new Exception\DomainException('exception message', 401);
209+
$exception->setTitle('problem title');
210+
$apiProblem = new ApiProblem('401', $exception);
211+
$payload = $apiProblem->toArray();
212+
$this->assertArrayHasKey('title', $payload);
213+
$this->assertEquals($exception->getTitle(), $payload['title']);
214+
}
215+
216+
public function testUsesTypeFromExceptionWhenProvided(): void
217+
{
218+
$exception = new Exception\DomainException('exception message', 401);
219+
$exception->setType('http://example.com/api/help/401');
220+
$apiProblem = new ApiProblem('401', $exception);
221+
$payload = $apiProblem->toArray();
222+
$this->assertArrayHasKey('type', $payload);
223+
$this->assertEquals($exception->getType(), $payload['type']);
224+
}
225+
226+
public function testUsesAdditionalDetailsFromExceptionWhenProvided(): void
227+
{
228+
$exception = new Exception\DomainException('exception message', 401);
229+
$exception->setAdditionalDetails(['foo' => 'bar']);
230+
$apiProblem = new ApiProblem('401', $exception);
231+
$payload = $apiProblem->toArray();
232+
$this->assertArrayHasKey('foo', $payload);
233+
$this->assertEquals('bar', $payload['foo']);
234+
}
235+
236+
/** @psalm-return array<string, array{0: int}> */
237+
public function invalidStatusCodes(): array
238+
{
239+
return [
240+
'-1' => [-1],
241+
'0' => [0],
242+
'7' => [7], // reported
243+
'14' => [14], // observed
244+
'600' => [600],
245+
];
246+
}
247+
248+
/**
249+
* @dataProvider invalidStatusCodes
250+
* @group api-tools-118
251+
*/
252+
public function testInvalidHttpStatusCodesAreCastTo500(int $code): void
253+
{
254+
$e = new \Exception('Testing', $code);
255+
$problem = new ApiProblem($code, $e);
256+
$this->assertEquals(500, $problem->status);
257+
}
258+
}

test/TestCase.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace ApiSkeletonsTest\Laravel\ApiProblem;
4+
5+
use Orchestra\Testbench\TestCase as OrchestraTestCase;
6+
7+
abstract class TestCase extends OrchestraTestCase
8+
{
9+
protected function getPackageProviders($app)
10+
{
11+
return [
12+
\ApiSkeletons\Laravel\ApiProblem\ServiceProvider::class,
13+
];
14+
}
15+
16+
protected function getEnvironmentSetUp($app)
17+
{
18+
}
19+
}

0 commit comments

Comments
 (0)