Skip to content

Commit c5be669

Browse files
committed
Fix location header handling and add tests for successful resource creation #148
1 parent 6e26156 commit c5be669

File tree

3 files changed

+94
-42
lines changed

3 files changed

+94
-42
lines changed

src/Helper.php

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static function prepareReturn(Arrayable $object, ResourceType $resourceTy
5959

6060
public static function objectToSCIMArray($object, ResourceType $resourceType = null, array $attributes = [], array $excludedAttributes = [])
6161
{
62-
if($resourceType == null){
62+
if ($resourceType == null) {
6363
$result = $object instanceof Arrayable ? $object->toArray() : $object;
6464

6565
if (is_array($result) && !empty($excludedAttributes)) {
@@ -81,7 +81,7 @@ public static function objectToSCIMArray($object, ResourceType $resourceType = n
8181

8282
// Move main schema to the top. It may not be defined, for example when only specific attributes are requested.
8383
$main = $result[$defaultSchema] ?? [];
84-
84+
8585
unset($result[$defaultSchema]);
8686

8787
$result = array_merge($result, $main);
@@ -106,42 +106,14 @@ public static function getResourceObjectVersion($object)
106106
}
107107

108108
/**
109-
*
110-
* @param unknown $object
109+
* @param Model $object
111110
* @param ResourceType $resourceType
112111
*/
113112
public static function objectToSCIMResponse(Model $object, ResourceType $resourceType = null, array $attributes = [], array $excludedAttributes = [])
114113
{
115114
$response = response(self::objectToSCIMArray($object, $resourceType, $attributes, $excludedAttributes))
116115
->header('ETag', self::getResourceObjectVersion($object));
117116

118-
if ($resourceType !== null) {
119-
$resourceTypeName = $resourceType->getName();
120-
121-
if ($resourceTypeName === null) {
122-
$routeResourceType = request()?->route('resourceType');
123-
124-
if ($routeResourceType instanceof ResourceType) {
125-
$resourceTypeName = $routeResourceType->getName();
126-
} elseif (is_string($routeResourceType)) {
127-
$resourceTypeName = $routeResourceType;
128-
}
129-
}
130-
131-
if ($resourceTypeName !== null) {
132-
$response->header(
133-
'Location',
134-
route(
135-
'scim.resource',
136-
[
137-
'resourceType' => $resourceTypeName,
138-
'resourceObject' => $object->getKey(),
139-
]
140-
)
141-
);
142-
}
143-
}
144-
145117
return $response;
146118
}
147119

src/Http/Controllers/ResourceController.php

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,16 @@ public function create(Request $request, PolicyDecisionPoint $pdp, ResourceType
103103
{
104104
$resourceObject = $this->createObject($request, $pdp, $resourceType, $isMe);
105105

106-
return $this->respondWithResource($request, $resourceType, $resourceObject, 201);
106+
return $this->respondWithResource($request, $resourceType, $resourceObject, 201)->header(
107+
'Location',
108+
route(
109+
'scim.resource',
110+
[
111+
'resourceType' => $resourceType->getName(),
112+
'resourceObject' => $resourceObject->getKey(),
113+
]
114+
)
115+
);
107116
}
108117

109118
public function show(Request $request, PolicyDecisionPoint $pdp, ResourceType $resourceType, Model $resourceObject)
@@ -260,33 +269,33 @@ function (Builder $query) use ($filter, $resourceType) {
260269
if (!$cursorPaginationEnabled) {
261270
throw (new SCIMException('Cursor pagination is disabled.'))->setCode(400)->setScimType('invalidCursor');
262271
}
263-
if($sortBy == null){
272+
if ($sortBy == null) {
264273
$resourceObjects = $resourceObjects->orderBy('id');
265274
}
266275

267-
if($request->input('cursor')){
276+
if ($request->input('cursor')) {
268277
$cursor = @Cursor::fromEncoded($request->input('cursor'));
269278

270-
if($cursor == null){
279+
if ($cursor == null) {
271280
throw (new SCIMException('Invalid Cursor'))->setCode(400)->setScimType('invalidCursor');
272281
}
273282
}
274283

275284
$countRaw = $request->input('count');
276285

277-
if($countRaw < 1 || $countRaw > config('scim.pagination.maxPageSize')){
286+
if ($countRaw < 1 || $countRaw > config('scim.pagination.maxPageSize')) {
278287
throw (new SCIMException(
279288
sprintf('Count value is invalid. Count value must be between 1 - and maxPageSize (%s) (when using cursor pagination)', config('scim.pagination.maxPageSize'))
280289
))->setCode(400)->setScimType('invalidCount');
281290
}
282-
291+
283292
$resourceObjects = $resourceObjects->cursorPaginate(
284293
$count,
285294
cursor: $request->input('cursor')
286295
);
287296
$resources = collect($resourceObjects->items());
288297

289-
298+
290299
} else {
291300
// The 1-based index of the first query result. A value less than 1 SHALL be interpreted as 1.
292301
$startIndex = max(1, intVal($request->input('startIndex', 0)));
@@ -327,7 +336,8 @@ public function crossResourceIndex(Request $request, PolicyDecisionPoint $pdp, S
327336
return $this->runCrossResourceQuery($request, $config);
328337
}
329338

330-
public function search(Request $request, PolicyDecisionPoint $pdp, ResourceType $resourceType){
339+
public function search(Request $request, PolicyDecisionPoint $pdp, ResourceType $resourceType)
340+
{
331341

332342
$input = $request->json()->all();
333343

@@ -466,8 +476,12 @@ protected function respondWithResource(Request $request, ResourceType $resourceT
466476
{
467477
[$attributes, $excludedAttributes] = $this->resolveAttributeParameters($request);
468478

469-
return Helper::objectToSCIMResponse($resourceObject, $resourceType, $attributes, $excludedAttributes)
470-
->setStatusCode($status);
479+
return Helper::objectToSCIMResponse(
480+
$resourceObject,
481+
$resourceType,
482+
$attributes,
483+
$excludedAttributes
484+
)->setStatusCode($status);
471485
}
472486

473487
protected function resolveAttributeParameters(Request $request): array
@@ -520,7 +534,7 @@ private function normalizeAttributeList($value): array
520534
if (is_string($value)) {
521535
$parts = preg_split('/\s*,\s*/', $value);
522536

523-
$normalized = array_filter(array_map('trim', $parts), fn ($item) => $item !== '');
537+
$normalized = array_filter(array_map('trim', $parts), fn($item) => $item !== '');
524538

525539
return array_values(array_unique($normalized));
526540
}

tests/HeaderTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,70 @@ public function testPut(){
5555
], ['If-Match' => $etag]);
5656
$response->assertStatus(200);
5757
}
58+
59+
public function testLocationHeaderOnlyReturnedOnSuccessfulCreate()
60+
{
61+
$createResponse = $this->post('/scim/v2/Users', $this->validUserPayload());
62+
$createResponse->assertStatus(201);
63+
$createResponse->assertHeader('Location');
64+
65+
$locationHeader = $createResponse->baseResponse->headers->get('Location');
66+
$this->assertMatchesRegularExpression('#^http://localhost/scim/v2/Users/\d+$#', $locationHeader);
67+
68+
$userId = $createResponse->json('id');
69+
$this->assertNotEmpty($userId, 'Created SCIM resource is missing an id');
70+
71+
$getResponse = $this->get("/scim/v2/Users/{$userId}");
72+
$getResponse->assertStatus(200);
73+
$getResponse->assertHeaderMissing('Location');
74+
$etag = $getResponse->baseResponse->headers->get('ETag');
75+
$this->assertNotEmpty($etag, 'GET response should expose an ETag for optimistic locking');
76+
77+
$putResponse = $this->put(
78+
"/scim/v2/Users/{$userId}",
79+
[
80+
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User'],
81+
'userName' => 'location-put-' . uniqid(),
82+
'id' => (string) $userId,
83+
],
84+
['If-Match' => $etag]
85+
);
86+
$putResponse->assertStatus(200);
87+
$putResponse->assertHeaderMissing('Location');
88+
89+
$patchResponse = $this->patch("/scim/v2/Users/{$userId}", [
90+
'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
91+
'Operations' => [[
92+
'op' => 'add',
93+
'path' => 'userName',
94+
'value' => 'location-patch-' . uniqid(),
95+
]],
96+
]);
97+
$patchResponse->assertStatus(200);
98+
$patchResponse->assertHeaderMissing('Location');
99+
100+
$deleteResponse = $this->delete("/scim/v2/Users/{$userId}");
101+
$deleteResponse->assertStatus(204);
102+
$deleteResponse->assertHeaderMissing('Location');
103+
}
104+
105+
protected function validUserPayload(?string $email = null): array
106+
{
107+
$email = $email ?? sprintf('location.%[email protected]', uniqid());
108+
109+
return [
110+
'schemas' => [
111+
'urn:ietf:params:scim:schemas:core:2.0:User',
112+
],
113+
'urn:ietf:params:scim:schemas:core:2.0:User' => [
114+
'userName' => 'location-user-' . uniqid(),
115+
'password' => 'Password123',
116+
'emails' => [[
117+
'value' => $email,
118+
'type' => 'work',
119+
'primary' => true,
120+
]],
121+
],
122+
];
123+
}
58124
}

0 commit comments

Comments
 (0)