1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 20:04:24 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Alexander Skvortsov
f2aa804753 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-11-13 07:56:37 +00:00
Alexander Skvortsov
baa5fdad95 Add extender and tests for filter 2020-11-13 02:56:13 -05:00
Alexander Skvortsov
0b2d01b75f Define FilterInterface 2020-11-13 02:55:58 -05:00
Alexander Skvortsov
84c4e8dd6e Ensure that search AND list endpoints are tested for users and discussions 2020-11-13 02:55:17 -05:00
Alexander Skvortsov
c4daeeb1f2 Add abstract filterer system, have discussions and users controllers use that instead of default filterer system 2020-11-13 01:47:15 -05:00
Alexander Skvortsov
87d2f3d246 If a search parameter is present (filter.q), send requests to the search endpoint instead of the filter endpoint 2020-11-12 14:29:00 -05:00
Alexander Skvortsov
0172008693 Add Search controllers for Discussions and Users, as copies of the ListDiscussions and ListUsers controllers 2020-11-12 14:20:18 -05:00
Alexander Skvortsov
76680d197d Introduce RequestUtil
Abstracts access to following request attributes:

- actor
- session
- locale
- route name
2020-11-11 17:16:56 -05:00
19 changed files with 915 additions and 137 deletions

View File

@@ -83,7 +83,7 @@ export default class Store {
*/
find(type, id, query = {}, options = {}) {
let params = query;
let url = app.forum.attribute('apiUrl') + '/' + type;
let url = app.forum.attribute('apiUrl') + (query.search ? '/search/' : '/') + type;
if (id instanceof Array) {
url += '?filter[id]=' + id.join(',');

View File

@@ -24,7 +24,7 @@ export default class DiscussionsSearchSource {
include: 'mostRelevantPost',
};
return app.store.find('discussions', params).then((results) => (this.results[query] = results));
return app.store.find('discussions', params, { search: query }).then((results) => (this.results[query] = results));
}
view(query) {

View File

@@ -16,10 +16,14 @@ export default class UsersSearchResults {
search(query) {
return app.store
.find('users', {
filter: { q: query },
page: { limit: 5 },
})
.find(
'users',
{
filter: { q: query },
page: { limit: 5 },
},
{ search: query }
)
.then((results) => {
this.results[query] = results;
m.redraw();

View File

@@ -119,7 +119,7 @@ export default class DiscussionListState {
params.page = { offset };
params.include = params.include.join(',');
return this.app.store.find('discussions', params);
return this.app.store.find('discussions', params, { search: params.filter.q });
}
/**

View File

@@ -11,10 +11,10 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -49,9 +49,14 @@ class ListDiscussionsController extends AbstractListController
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
* @var DiscussionRepository
*/
protected $searcher;
protected $discussions;
/**
* @var Filterer
*/
protected $filterer;
/**
* @var UrlGenerator
@@ -62,9 +67,10 @@ class ListDiscussionsController extends AbstractListController
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
public function __construct(DiscussionRepository $discussions, Filterer $filterer, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->discussions = $discussions;
$this->filterer = $filterer;
$this->url = $url;
}
@@ -74,16 +80,16 @@ class ListDiscussionsController extends AbstractListController
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$query = $this->discussions->query();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),

View File

@@ -10,10 +10,9 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Flarum\User\UserRepository;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -41,9 +40,9 @@ class ListUsersController extends AbstractListController
];
/**
* @var UserSearcher
* @var Filterer
*/
protected $searcher;
protected $filterer;
/**
* @var UrlGenerator
@@ -51,13 +50,20 @@ class ListUsersController extends AbstractListController
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
* @var UserRepository
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
protected $users;
/**
* @param Filterer $filterer
* @param UrlGenerator $url
* @param UserRepository $users
*/
public function __construct(Filterer $filterer, UrlGenerator $url, UserRepository $users)
{
$this->searcher = $searcher;
$this->filterer = $filterer;
$this->url = $url;
$this->users = $users;
}
/**
@@ -69,16 +75,16 @@ class ListUsersController extends AbstractListController
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$query = $this->users->query();
$criteria = new SearchCriteria($actor, $query, $sort);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),

View File

@@ -0,0 +1,112 @@
<?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\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchDiscussionsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = DiscussionSerializer::class;
/**
* {@inheritdoc}
*/
public $include = [
'user',
'lastPostedUser',
'mostRelevantPost',
'mostRelevantPost.user'
];
/**
* {@inheritdoc}
*/
public $optionalInclude = [
'firstPost',
'lastPost'
];
/**
* {@inheritdoc}
*/
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
Discussion::setStateUser($actor);
$results = $results->getResults()->load($load);
if ($relations = array_intersect($load, ['firstPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
}
}

View File

@@ -0,0 +1,93 @@
<?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\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchUsersController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = UserSerializer::class;
/**
* {@inheritdoc}
*/
public $include = ['groups'];
/**
* {@inheritdoc}
*/
public $sortFields = [
'username',
'commentCount',
'discussionCount',
'lastSeenAt',
'joinedAt'
];
/**
* @var UserSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
return $results->getResults();
}
}

View File

@@ -95,6 +95,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\SendConfirmationEmailController::class)
);
// List users
$map->get(
'/search/users',
'users.search',
$route->toController(Controller\SearchUsersController::class)
);
/*
|--------------------------------------------------------------------------
| Notifications
@@ -163,6 +170,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\DeleteDiscussionController::class)
);
// Search discussions
$map->get(
'/search/discussions',
'discussions.search',
$route->toController(Controller\SearchDiscussionsController::class)
);
/*
|--------------------------------------------------------------------------
| Posts

63
src/Extend/Filter.php Normal file
View File

@@ -0,0 +1,63 @@
<?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\Extend;
use Flarum\Extension\Extension;
use Flarum\Filter\Filterer;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
class Filter implements ExtenderInterface
{
private $resource;
private $filters = [];
private $filterMutators = [];
/**
* @param string $resource: The ::class attribute of the resource this applies to, which is typically an Eloquent model.
*/
public function __construct($resource)
{
$this->resource = $resource;
}
/**
* Add a filter to run when the resource is filtered.
*
* @param string $filterClass: The ::class attribute of the filter you are adding.
*/
public function addFilter(string $filterClass)
{
$this->filters[] = $filterClass;
return $this;
}
/**
* Add a callback through which to run all filter queries after filters have been applied.
*/
public function addFilterMutator($callback)
{
$this->filterMutators[] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->filters as $filter) {
Filterer::addFilter($this->resource, $container->make($filter));
}
foreach ($this->filterMutators as $mutator) {
Filterer::addFilterMutator($this->resource, ContainerUtil::wrapCallback($mutator, $container));
}
}
}

View File

@@ -0,0 +1,26 @@
<?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\Filter;
interface FilterInterface
{
/**
* This filter will only be run when a query contains a filter param with this key.
*/
public function getKey(): string;
/**
* Filters a query.
*
* @param WrappedFilter $filter
* @param string $value The value of the requested filter
*/
public function apply(WrappedFilter $wrappedFilter, $filterValue);
}

88
src/Filter/Filterer.php Normal file
View File

@@ -0,0 +1,88 @@
<?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\Filter;
use Flarum\Search\ApplySearchParametersTrait;
use Flarum\Search\SearchResults;
use Illuminate\Support\Arr;
class Filterer
{
use ApplySearchParametersTrait;
protected static $filters = [];
protected static $filterMutators = [];
public static function addFilter($resource, FilterInterface $filter)
{
if (! array_key_exists($resource, static::$filters)) {
static::$filters[$resource] = [];
}
if (! array_key_exists($filter->getKey(), static::$filters[$resource])) {
static::$filters[$resource][$filter->getKey()] = [];
}
static::$filters[$resource][$filter->getKey()][] = $filter;
}
public static function addFilterMutator($resource, $mutator)
{
if (! array_key_exists($resource, static::$filterMutators)) {
static::$filterMutators[$resource] = [];
}
static::$filterMutators[$resource][] = $mutator;
}
/**
* @param FilterCriteria $criteria
* @param int|null $limit
* @param int $offset
*
* @return FilterResults
*/
public function filter($actor, $query, $filters, $sort = null, $limit = null, $offset = 0, array $load = [])
{
$resource = get_class($query->getModel());
$query->whereVisibleTo($actor);
$wrappedFilter = new WrappedFilter($query->getQuery(), $actor);
foreach ($filters as $filterKey => $filterValue) {
foreach (Arr::get(static::$filters, "$resource.$filterKey", []) as $filter) {
$filter->apply($wrappedFilter, $filterValue);
}
}
$this->applySort($wrappedFilter, $sort);
$this->applyOffset($wrappedFilter, $offset);
$this->applyLimit($wrappedFilter, $limit + 1);
foreach (Arr::get(static::$filterMutators, $resource, []) as $mutator) {
$mutator($query, $actor, $filters, $sort);
}
// Execute the filter query and retrieve the results. We get one more
// results than the user asked for, so that we can say if there are more
// results. If there are, we will get rid of that extra result.
$results = $query->get();
if ($areMoreResults = $limit > 0 && $results->count() > $limit) {
$results->pop();
}
$results->load($load);
return new SearchResults($results, $areMoreResults);
}
}

View File

@@ -0,0 +1,16 @@
<?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\Filter;
use Flarum\Search\AbstractSearch;
class WrappedFilter extends AbstractSearch
{
}

View File

@@ -11,6 +11,7 @@ namespace Flarum\Forum\Content;
use Flarum\Api\Client;
use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Api\Controller\SearchDiscussionsController;
use Flarum\Frontend\Document;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
@@ -114,6 +115,6 @@ class Index
*/
private function getApiDocument(User $actor, array $params)
{
return json_decode($this->api->send(ListDiscussionsController::class, $actor, $params)->getBody());
return json_decode($this->api->send(($params['filter']['q'] ? SearchDiscussionsController::class : ListDiscussionsController::class), $actor, $params)->getBody());
}
}

57
src/Http/RequestUtil.php Normal file
View File

@@ -0,0 +1,57 @@
<?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\Http;
use Flarum\User\User;
use Illuminate\Contracts\Session\Session;
use Psr\Http\Message\ServerRequestInterface as Request;
class RequestUtil
{
public static function getActor(Request $request): User
{
return $request->getAttribute('actor');
}
public function withActor(Request $request, User $actor): Request
{
return $request->withAttribute('actor', $actor);
}
public function getSession(Request $request): Session
{
return $request->getAttribute('session');
}
public function withSession(Request $request, Session $session): Request
{
return $request->withAttribute('session', $session);
}
public function getLocale(Request $request): string
{
return $request->getAttribute('bypassCsrfToken');
}
public function withLocale(Request $request, string $locale): Request
{
return $request->withAttribute('locale', $locale);
}
public function getRouteName(Request $request): string
{
return $request->getAttribute('routeName');
}
public function withRouteName(Request $request, string $routeName): Request
{
return $request->withAttribute('routeName', $routeName);
}
}

View File

@@ -56,111 +56,19 @@ class ListTest extends TestCase
$this->assertEquals(1, count($data['data']));
}
/**
* @test
*/
public function can_search_for_author()
{
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => 'author:normal foo'],
'include' => 'mostRelevantPost',
])
);
// /**
// * @test
// */
// public function can_search_for_author()
// {
// $response = $this->send(
// $this->request('GET', '/api/search/discussions')
// ->withQueryParams([
// 'filter' => ['q' => 'author:normal foo'],
// 'include' => 'mostRelevantPost',
// ])
// );
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @test
*/
public function can_search_for_word_in_post()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function ignores_non_word_characters_when_searching()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail+'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function search_for_special_characters_gives_empty_result()
{
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => '*'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => '@'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
}
// $this->assertEquals(200, $response->getStatusCode());
// }
}

View File

@@ -0,0 +1,166 @@
<?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\Tests\integration\api\discussions;
use Carbon\Carbon;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
class SearchTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>'],
],
'users' => [
$this->normalUser(),
],
'groups' => [
$this->memberGroup(),
$this->guestGroup(),
],
'group_permission' => [
['permission' => 'viewDiscussions', 'group_id' => 2],
]
]);
}
/**
* @test
*/
public function shows_index_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/search/discussions')
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals(1, count($data['data']));
}
/**
* @test
*/
public function can_search_for_author()
{
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => 'author:normal foo'],
'include' => 'mostRelevantPost',
])
);
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @test
*/
public function can_search_for_word_in_post()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function ignores_non_word_characters_when_searching()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail+'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function search_for_special_characters_gives_empty_result()
{
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => '*'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => '@'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
}
}

View File

@@ -0,0 +1,83 @@
<?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\Tests\integration\api\users;
use Flarum\Group\Permission;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
class SearchTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'users' => [
$this->adminUser(),
],
'groups' => [
$this->adminGroup(),
$this->guestGroup(),
],
'group_permission' => [],
'group_user' => [
['user_id' => 1, 'group_id' => 1],
],
]);
}
/**
* @test
*/
public function disallows_index_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/search/users')
);
$this->assertEquals(403, $response->getStatusCode());
}
/**
* @test
*/
public function shows_index_for_guest_when_they_have_permission()
{
Permission::unguarded(function () {
Permission::create([
'permission' => 'viewUserList',
'group_id' => 2,
]);
});
$response = $this->send(
$this->request('GET', '/api/search/users')
);
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @test
*/
public function shows_index_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/search/users', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,135 @@
<?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\Tests\integration\extenders;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\WrappedFilter;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
class FilterTest extends TestCase
{
use RetrievesAuthorizedUsers;
public function prepDb()
{
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => 'DISCUSSION 1', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1],
['id' => 2, 'title' => 'DISCUSSION 2', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 2, 'comment_count' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>'],
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar not the same</p></t>'],
],
'users' => [
$this->adminUser(),
$this->normalUser(),
],
]);
}
public function filterDiscussions($filters, $limit = null)
{
$response = $this->send(
$this->request('GET', '/api/discussions', [
'authenticatedAs' => 1,
])->withQueryParams([
'filter' => $filters,
'include' => 'mostRelevantPost',
])
);
return json_decode($response->getBody()->getContents(), true)['data'];
}
/**
* @test
*/
public function works_as_expected_with_no_modifications()
{
$this->prepDb();
$searchForAll = json_encode($this->filterDiscussions([], 5));
$this->assertContains('DISCUSSION 1', $searchForAll);
$this->assertContains('DISCUSSION 2', $searchForAll);
}
/**
* @test
*/
public function custom_filter_gambit_has_effect_if_added()
{
$this->extend((new Extend\Filter(Discussion::class))->addFilter(NoResultFilter::class));
$this->prepDb();
$withResultSearch = json_encode($this->filterDiscussions(['noResult' => 0], 5));
$this->assertContains('DISCUSSION 1', $withResultSearch);
$this->assertContains('DISCUSSION 2', $withResultSearch);
$this->assertEquals([], $this->filterDiscussions(['noResult' => 1], 5));
}
/**
* @test
*/
public function filter_mutator_has_effect_if_added()
{
$this->extend((new Extend\Filter(Discussion::class))->addFilterMutator(function ($query, $actor, $filters, $sort) {
$query->getQuery()->whereRaw('1=0');
}));
$this->prepDb();
$this->assertEquals([], $this->filterDiscussions([], 5));
}
/**
* @test
*/
public function filter_mutator_has_effect_if_added_with_invokable_class()
{
$this->extend((new Extend\Filter(Discussion::class))->addFilterMutator(CustomFilterMutator::class));
$this->prepDb();
$this->assertEquals([], $this->filterDiscussions([], 5));
}
}
class NoResultFilter implements FilterInterface
{
public function getKey(): string
{
return 'noResult';
}
/**
* {@inheritdoc}
*/
public function apply(WrappedFilter $wrappedFilter, $filterValue)
{
if ($filterValue) {
$wrappedFilter->getQuery()
->whereRaw('0=1');
}
}
}
class CustomFilterMutator
{
public function __invoke($query, $actor, $filters, $sort)
{
$query->getQuery()->whereRaw('1=0');
}
}