Skip to content

Commit 8717d03

Browse files
committed
[Bugfix] Resolve correct query class in attach and detach actions
Closes #102
1 parent 7085482 commit 8717d03

File tree

5 files changed

+400
-2
lines changed

5 files changed

+400
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ All notable changes to this project will be documented in this file. This projec
3030

3131
- [#101](https://github.com/laravel-json-api/laravel/issues/101) Ensure controller create action always returns a
3232
response that will result in a `201 Created` response.
33+
- [#102](https://github.com/laravel-json-api/laravel/issues/102) The attach and detach to-many relationship controller
34+
actions now correctly resolve the collection query class using the relation's inverse resource type. Previously they
35+
were incorrectly using the primary resource type to resolve the query class.
3336

3437
## [1.0.0-beta.4] - 2021-06-02
3538

src/Http/Controllers/Actions/AttachRelationship.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function attachRelationship(Route $route, StoreContract $store)
5252
$resourceType = $route->resourceType()
5353
);
5454

55-
$query = ResourceQuery::queryMany($resourceType);
55+
$query = ResourceQuery::queryMany($relation->inverse());
5656

5757
$model = $route->model();
5858
$response = null;

src/Http/Controllers/Actions/DetachRelationship.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function detachRelationship(Route $route, StoreContract $store)
5252
$resourceType = $route->resourceType()
5353
);
5454

55-
$query = ResourceQuery::queryMany($resourceType);
55+
$query = ResourceQuery::queryMany($relation->inverse());
5656

5757
$model = $route->model();
5858
$response = null;
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
/*
3+
* Copyright 2021 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\Tests\Api\V1\Posts;
21+
22+
use App\Models\Comment;
23+
use App\Models\Image;
24+
use App\Models\Post;
25+
use App\Models\User;
26+
use App\Models\Video;
27+
use App\Tests\Api\V1\TestCase;
28+
29+
class AttachMediaTest extends TestCase
30+
{
31+
32+
/**
33+
* @var Post
34+
*/
35+
private Post $post;
36+
37+
/**
38+
* @return void
39+
*/
40+
protected function setUp(): void
41+
{
42+
parent::setUp();
43+
$this->post = Post::factory()->create();
44+
}
45+
46+
public function test(): void
47+
{
48+
$existingImages = Image::factory()->count(2)->create();
49+
$existingVideos = Video::factory()->count(2)->create();
50+
51+
$this->post->images()->saveMany($existingImages);
52+
$this->post->videos()->saveMany($existingVideos);
53+
54+
$images = Image::factory()->count(2)->create();
55+
$videos = Video::factory()->count(2)->create();
56+
57+
$ids = collect($images)->merge($videos)->map(fn($model) => [
58+
'type' => ($model instanceof Image) ? 'images' : 'videos',
59+
'id' => (string) $model->getRouteKey(),
60+
])->all();
61+
62+
$response = $this
63+
->withoutExceptionHandling()
64+
->actingAs($this->post->author)
65+
->jsonApi('videos') // @TODO assertions should work without this.
66+
->withData($ids)
67+
->post(url('/api/v1/posts', [$this->post, 'relationships', 'media']));
68+
69+
$response->assertNoContent();
70+
71+
$this->assertDatabaseCount('image_post', $images->count() + $existingImages->count());
72+
$this->assertDatabaseCount('post_video', $videos->count() + $existingVideos->count());
73+
74+
foreach ($existingImages->merge($images) as $image) {
75+
$this->assertDatabaseHas('image_post', [
76+
'image_uuid' => $image->getKey(),
77+
'post_id' => $this->post->getKey(),
78+
]);
79+
}
80+
81+
foreach ($existingVideos->merge($videos) as $video) {
82+
$this->assertDatabaseHas('post_video', [
83+
'post_id' => $this->post->getKey(),
84+
'video_uuid' => $video->getKey(),
85+
]);
86+
}
87+
}
88+
89+
public function testInvalid(): void
90+
{
91+
$comment = Comment::factory()->create();
92+
93+
$data = [
94+
[
95+
'type' => 'comments',
96+
'id' => (string) $comment->getRouteKey(),
97+
],
98+
];
99+
100+
$response = $this
101+
->actingAs($this->post->author)
102+
->jsonApi('videos')
103+
->withData($data)
104+
->post(url('/api/v1/posts', [$this->post, 'relationships', 'media']));
105+
106+
$response->assertExactErrorStatus([
107+
'detail' => 'The media field must be a to-many relationship containing images, videos resources.',
108+
'source' => ['pointer' => '/data/0'],
109+
'status' => '422',
110+
'title' => 'Unprocessable Entity',
111+
]);
112+
}
113+
114+
public function testUnauthorized(): void
115+
{
116+
$existingImages = Image::factory()->count(1)->create();
117+
$existingVideos = Video::factory()->count(1)->create();
118+
119+
$this->post->images()->saveMany($existingImages);
120+
$this->post->videos()->saveMany($existingVideos);
121+
122+
$images = Image::factory()->count(1)->create();
123+
$videos = Video::factory()->count(1)->create();
124+
125+
$ids = collect($images)->merge($videos)->map(fn($model) => [
126+
'type' => ($model instanceof Image) ? 'images' : 'videos',
127+
'id' => (string) $model->getRouteKey(),
128+
])->all();
129+
130+
$response = $this
131+
->jsonApi('videos')
132+
->withData($ids)
133+
->post(url('/api/v1/posts', [$this->post, 'relationships', 'media']));
134+
135+
$response->assertStatus(401);
136+
137+
$this->assertDatabaseCount('image_post', 1);
138+
$this->assertDatabaseCount('post_video', 1);
139+
}
140+
141+
public function testForbidden(): void
142+
{
143+
$existingImages = Image::factory()->count(1)->create();
144+
$existingVideos = Video::factory()->count(1)->create();
145+
146+
$this->post->images()->saveMany($existingImages);
147+
$this->post->videos()->saveMany($existingVideos);
148+
149+
$images = Image::factory()->count(1)->create();
150+
$videos = Video::factory()->count(1)->create();
151+
152+
$ids = collect($images)->merge($videos)->map(fn($model) => [
153+
'type' => ($model instanceof Image) ? 'images' : 'videos',
154+
'id' => (string) $model->getRouteKey(),
155+
])->all();
156+
157+
$response = $this
158+
->actingAs(User::factory()->create())
159+
->jsonApi('videos')
160+
->withData($ids)
161+
->post(url('/api/v1/posts', [$this->post, 'relationships', 'media']));
162+
163+
$response->assertStatus(403);
164+
165+
$this->assertDatabaseCount('image_post', 1);
166+
$this->assertDatabaseCount('post_video', 1);
167+
}
168+
169+
public function testNotAcceptableMediaType(): void
170+
{
171+
$response = $this
172+
->actingAs($this->post->author)
173+
->jsonApi('posts')
174+
->accept('text/html')
175+
->withData([])
176+
->post(url('/api/v1/posts', [$this->post, 'relationships', 'media']));
177+
178+
$response->assertStatus(406);
179+
}
180+
181+
public function testUnsupportedMediaType(): void
182+
{
183+
$response = $this
184+
->actingAs($this->post->author)
185+
->jsonApi('posts')
186+
->contentType('application/json')
187+
->withData([])
188+
->post(url('/api/v1/posts', [$this->post, 'relationships', 'media']));
189+
190+
$response->assertStatus(415);
191+
}
192+
}

0 commit comments

Comments
 (0)