Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: JSON:API #3971

Merged
merged 52 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5e1f971
refactor: json:api refactor iteration 1
SychO9 Feb 10, 2024
4e3b5f7
chore: delete dead code
SychO9 Feb 10, 2024
bc5bac0
fix: regressions
SychO9 Feb 16, 2024
dc71b82
chore: move additions/changes to package
SychO9 Feb 17, 2024
0619662
feat: AccessTokenResource
SychO9 Feb 17, 2024
c36f034
feat: allow dependency injection in resources
SychO9 Feb 17, 2024
c5b61a1
feat: `ApiResource` extender
SychO9 Feb 21, 2024
cd95879
feat: improve
SychO9 Feb 23, 2024
82b9c54
feat: refactor tags extension
SychO9 Feb 23, 2024
8bcc2ff
feat: refactor flags extension
SychO9 Feb 26, 2024
f6cd055
fix: regressions
SychO9 Feb 26, 2024
8b7f3c3
fix: drop bc layer
SychO9 Feb 26, 2024
6e753b4
feat: refactor suspend extension
SychO9 Feb 26, 2024
aebd527
feat: refactor subscriptions extension
SychO9 Feb 26, 2024
d0d3c15
feat: refactor approval extension
SychO9 Feb 26, 2024
7e0ff2a
feat: refactor sticky extension
SychO9 Feb 26, 2024
8a6e96d
feat: refactor nicknames extension
SychO9 Feb 26, 2024
14955bb
feat: refactor mentions extension
SychO9 Mar 1, 2024
e19346e
feat: refactor lock extension
SychO9 Mar 1, 2024
40d219e
feat: refactor likes extension
SychO9 Mar 1, 2024
78ed7d5
Merge branch '2.x' into sm/json-api-server
SychO9 Mar 2, 2024
6f942ad
chore: merge conflicts
SychO9 Mar 2, 2024
3644d81
feat: refactor extension-manager extension
SychO9 Mar 2, 2024
06364a4
feat: context current endpoint helpers
SychO9 Mar 2, 2024
26b1185
chore: minor
SychO9 Mar 2, 2024
fdf7b9a
feat: cleaner sortmap implementation
SychO9 Mar 2, 2024
3493dc8
chore: drop old package
SychO9 Mar 5, 2024
a776510
chore: not needed (auto scoping)
SychO9 Mar 5, 2024
f52c623
fix: actor only fields
SychO9 Mar 6, 2024
7756e33
refactor: simplify index endpoint
SychO9 Mar 6, 2024
d2bbd83
feat: eager loading
SychO9 Mar 8, 2024
5b0dd88
Apply fixes from StyleCI
StyleCIBot Mar 8, 2024
16b4975
test: adapt
SychO9 Mar 8, 2024
4a9985d
Apply fixes from StyleCI
StyleCIBot Mar 8, 2024
e0f8f53
test: phpstan
SychO9 Mar 8, 2024
c88b1f4
Apply fixes from StyleCI
StyleCIBot Mar 8, 2024
cda2b37
test: adapt
SychO9 Mar 8, 2024
7c4f69e
fix: typing
SychO9 Mar 8, 2024
1fe426a
fix: approving content
SychO9 Mar 8, 2024
f3b8190
tet: adapt frontend tests
SychO9 Mar 8, 2024
80ded88
chore: typings
SychO9 Mar 8, 2024
7ea25d3
chore: review
SychO9 Mar 28, 2024
df05db6
Apply fixes from StyleCI
StyleCIBot Mar 28, 2024
0549555
fix: breaking change
SychO9 Mar 28, 2024
caaaadd
Merge branch 'refs/heads/2.x' into sm/json-api-server
SychO9 May 9, 2024
d4d3c98
Apply fixes from StyleCI
StyleCIBot May 9, 2024
06fef7e
fix
SychO9 May 9, 2024
bf5b522
fix
SychO9 May 10, 2024
00d4a29
fix
SychO9 May 10, 2024
71c20ef
fix
SychO9 May 10, 2024
0482ce9
Merge branch 'refs/heads/2.x' into sm/json-api-server
SychO9 Jun 21, 2024
6ab9adb
chore
SychO9 Jun 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"php": "^8.1",
"ext-json": "*",
"components/font-awesome": "^5.15.0",
"composer/composer": "^2.0",
"composer/composer": "^2.7",
"dflydev/fig-cookies": "^3.0",
"doctrine/dbal": "^3.6.2",
"dragonmantank/cron-expression": "^3.3",
Expand Down Expand Up @@ -151,7 +151,6 @@
"pusher/pusher-php-server": "^7.2",
"s9e/text-formatter": "^2.13",
"staudenmeir/eloquent-eager-limit": "^1.8.2",
"sycho/json-api": "^0.5.0",
"sycho/sourcemap": "^2.0.0",
"symfony/config": "^6.3",
"symfony/console": "^6.3",
Expand All @@ -163,6 +162,7 @@
"symfony/postmark-mailer": "^6.3",
"symfony/translation": "^6.3",
"symfony/yaml": "^6.3",
"flarum/json-api-server": "^1.0.0",
"wikimedia/less.php": "^4.1"
},
"require-dev": {
Expand Down
21 changes: 9 additions & 12 deletions extensions/approval/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* LICENSE file that was distributed with this source code.
*/

use Flarum\Api\Serializer\BasicDiscussionSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Api\Resource;
use Flarum\Api\Schema;
use Flarum\Approval\Access;
use Flarum\Approval\Api\PostResourceFields;
use Flarum\Approval\Event\PostWasApproved;
use Flarum\Approval\Listener;
use Flarum\Discussion\Discussion;
Expand All @@ -36,17 +37,13 @@
->default('is_approved', true)
->cast('is_approved', 'bool'),

(new Extend\ApiSerializer(BasicDiscussionSerializer::class))
->attribute('isApproved', function (BasicDiscussionSerializer $serializer, Discussion $discussion): bool {
return $discussion->is_approved;
}),
(new Extend\ApiResource(Resource\DiscussionResource::class))
->fields(fn () => [
Schema\Boolean::make('isApproved'),
]),

(new Extend\ApiSerializer(PostSerializer::class))
->attribute('isApproved', function ($serializer, Post $post) {
return (bool) $post->is_approved;
})->attribute('canApprove', function (PostSerializer $serializer, Post $post) {
return (bool) $serializer->getActor()->can('approvePosts', $post->discussion);
}),
(new Extend\ApiResource(Resource\PostResource::class))
->fields(PostResourceFields::class),

new Extend\Locales(__DIR__.'/locale'),

Expand Down
29 changes: 29 additions & 0 deletions extensions/approval/src/Api/PostResourceFields.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Approval\Api;

use Flarum\Api\Context;
use Flarum\Api\Schema;
use Flarum\Post\Post;

class PostResourceFields
{
public function __invoke(): array
{
return [
Schema\Boolean::make('isApproved')
->writable(fn (Post $post, Context $context) => $context->getActor()->can('approve', $post))
// set by the ApproveContent listener.
->set(fn () => null),
Schema\Boolean::make('canApprove')
->get(fn (Post $post, Context $context) => $context->getActor()->can('approvePosts', $post->discussion)),
];
}
}
123 changes: 123 additions & 0 deletions extensions/approval/tests/integration/api/ApprovePostsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Approval\Tests\integration\api;

use Carbon\Carbon;
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;

class ApprovePostsTest extends TestCase
{
use RetrievesAuthorizedUsers;
use InteractsWithUnapprovedContent;

protected function setUp(): void
{
parent::setUp();

$this->extension('flarum-approval');

$this->prepareDatabase([
'users' => [
['id' => 1, 'username' => 'Muralf', 'email' => '[email protected]', 'is_email_confirmed' => 1],
$this->normalUser(),
['id' => 3, 'username' => 'acme', 'email' => '[email protected]', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'luceos', 'email' => '[email protected]', 'is_email_confirmed' => 1],
],
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 1],
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 2],
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 3],
['id' => 4, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => Carbon::now(), 'is_approved' => 1, 'number' => 4],
['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 5],
],
'groups' => [
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
],
'group_user' => [
['user_id' => 3, 'group_id' => 4],
],
'group_permission' => [
['group_id' => 4, 'permission' => 'discussion.approvePosts'],
]
]);
}

/**
* @test
*/
public function can_approve_unapproved_post()
{
$response = $this->send(
$this->request('PATCH', '/api/posts/3', [
'authenticatedAs' => 3,
'json' => [
'data' => [
'attributes' => [
'isApproved' => true
]
]
]
])
);

$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
$this->assertEquals(1, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
}

/**
* @test
*/
public function cannot_approve_post_without_permission()
{
$response = $this->send(
$this->request('PATCH', '/api/posts/3', [
'authenticatedAs' => 4,
'json' => [
'data' => [
'attributes' => [
'isApproved' => true
]
]
]
])
);

$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
$this->assertEquals(0, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
}

/**
* @test
*/
public function hiding_post_silently_approves_it()
{
$response = $this->send(
$this->request('PATCH', '/api/posts/5', [
'authenticatedAs' => 3,
'json' => [
'data' => [
'attributes' => [
'isHidden' => true
]
]
]
])
);

$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
$this->assertEquals(1, $this->database()->table('posts')->where('id', 5)->where('is_approved', 1)->count());
}
}
153 changes: 153 additions & 0 deletions extensions/approval/tests/integration/api/CreatePostsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Approval\Tests\integration\api;

use Carbon\Carbon;
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
use Flarum\Group\Group;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;

class CreatePostsTest extends TestCase
{
use RetrievesAuthorizedUsers;
use InteractsWithUnapprovedContent;

protected function setUp(): void
{
parent::setUp();

$this->extension('flarum-flags', 'flarum-approval');

$this->prepareDatabase([
'users' => [
['id' => 1, 'username' => 'Muralf', 'email' => '[email protected]', 'is_email_confirmed' => 1],
$this->normalUser(),
['id' => 3, 'username' => 'acme', 'email' => '[email protected]', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'luceos', 'email' => '[email protected]', 'is_email_confirmed' => 1],
],
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0],
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
['id' => 4, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
['id' => 5, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
['id' => 6, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
['id' => 7, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
['id' => 8, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
['id' => 9, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 0, 'number' => 3],
],
'groups' => [
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
],
'group_user' => [
['user_id' => 3, 'group_id' => 4],
['user_id' => 2, 'group_id' => 5],
],
'group_permission' => [
['group_id' => 4, 'permission' => 'discussion.startWithoutApproval'],
['group_id' => 5, 'permission' => 'discussion.replyWithoutApproval'],
]
]);
}

/**
* @dataProvider startDiscussionDataProvider
* @test
*/
public function can_start_discussion_without_approval_when_allowed(int $authenticatedAs, bool $allowed)
{
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.startWithoutApproval')->delete();

$response = $this->send(
$this->request('POST', '/api/discussions', [
'authenticatedAs' => $authenticatedAs,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'This is a new discussion',
'content' => 'This is a new discussion',
]
]
]
])
);

$body = $response->getBody()->getContents();
$json = json_decode($body, true);

$this->assertEquals(201, $response->getStatusCode(), $body);
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('discussions')->where('id', $json['data']['id'])->value('is_approved'));
}

/**
* @dataProvider replyToDiscussionDataProvider
* @test
*/
public function can_reply_without_approval_when_allowed(?int $authenticatedAs, bool $allowed)
{
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.replyWithoutApproval')->delete();

$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => $authenticatedAs,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'This is a new reply',
],
'relationships' => [
'discussion' => [
'data' => [
'type' => 'discussions',
'id' => 1
]
]
]
]
]
])
);

$body = $response->getBody()->getContents();
$json = json_decode($body, true);

$this->assertEquals(201, $response->getStatusCode(), $body);
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('posts')->where('id', $json['data']['id'])->value('is_approved'));
}

public static function startDiscussionDataProvider(): array
{
return [
'Admin' => [1, true],
'User without permission' => [2, false],
'Permission Given' => [3, true],
'Another user without permission' => [4, false],
];
}

public static function replyToDiscussionDataProvider(): array
{
return [
'Admin' => [1, true],
'User without permission' => [3, false],
'Permission Given' => [2, true],
'Another user without permission' => [4, false],
];
}
}
Loading
Loading