diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 45d55622..3f11fb32 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -9,6 +9,7 @@ use Blessing\Filter; use Blessing\Rejection; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Illuminate\Validation\Rule; class ReportController extends Controller @@ -65,46 +66,35 @@ class ReportController extends Controller public function manage(Request $request) { - $search = $request->input('search', ''); - $sortField = $request->input('sortField', 'report_at'); - $sortType = $request->input('sortType', 'desc'); - $page = $request->input('page', 1); - $perPage = $request->input('perPage', 10); + $q = $request->input('q'); - $reports = Report::where('tid', 'like', '%'.$search.'%') - ->orWhere('reporter', 'like', '%'.$search.'%') - ->orWhere('reason', 'like', '%'.$search.'%') - ->orderBy($sortField, $sortType) - ->offset(($page - 1) * $perPage) - ->limit($perPage) - ->get() - ->makeHidden(['informer']) - ->map(function ($report) { - $uploader = User::find($report->uploader); - if ($uploader) { - $report->uploaderName = $uploader->nickname; - } - if ($report->informer) { - $report->reporterName = $report->informer->nickname; - } + $pagination = Report::usingSearchString($q)->paginate(9); + $collection = $pagination->getCollection()->map(function ($report) { + $uploader = User::find($report->uploader); + if ($uploader) { + $report->uploaderName = $uploader->nickname; + } + if ($report->informer) { + $report->reporterName = $report->informer->nickname; + } + $report->getAttribute('texture'); - return $report; - }); + return $report; + }); + $pagination->setCollection($collection); - return [ - 'totalRecords' => Report::count(), - 'data' => $reports, - ]; + return $pagination; } - public function review(Request $request, Dispatcher $dispatcher) - { - $data = $this->validate($request, [ - 'id' => 'required|exists:reports', + public function review( + Report $report, + Request $request, + Dispatcher $dispatcher + ) { + $data = $request->validate([ 'action' => ['required', Rule::in(['delete', 'ban', 'reject'])], ]); $action = $data['action']; - $report = Report::find($data['id']); $dispatcher->dispatch('report.reviewing', [$report, $action]); @@ -127,8 +117,10 @@ class ReportController extends Controller switch ($action) { case 'delete': + /** @var Texture */ $texture = $report->texture; if ($texture) { + Storage::disk('textures')->delete($texture->hash); $texture->delete(); $dispatcher->dispatch('texture.deleted', [$texture]); } else { diff --git a/app/Models/Report.php b/app/Models/Report.php index 29769455..1f999a85 100644 --- a/app/Models/Report.php +++ b/app/Models/Report.php @@ -5,6 +5,7 @@ namespace App\Models; use DateTimeInterface; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; +use Lorisleiva\LaravelSearchString\Concerns\SearchString; /** * @property int $id @@ -19,6 +20,8 @@ use Illuminate\Support\Carbon; */ class Report extends Model { + use SearchString; + public const CREATED_AT = 'report_at'; public const UPDATED_AT = null; @@ -33,6 +36,12 @@ class Report extends Model 'status' => 'integer', ]; + protected $searchStringColumns = [ + 'id', 'tid', 'uploader', 'reporter', + 'reason', 'status', + 'report_at' => ['date' => true], + ]; + public function texture() { return $this->belongsTo(Texture::class, 'tid', 'tid'); diff --git a/app/Models/User.php b/app/Models/User.php index 8be064e6..e23a7082 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,7 +3,6 @@ namespace App\Models; use App\Models\Concerns\HasPassword; -use DateTimeInterface; use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; diff --git a/resources/assets/src/scripts/route.tsx b/resources/assets/src/scripts/route.tsx index 02798a6b..85941bb5 100644 --- a/resources/assets/src/scripts/route.tsx +++ b/resources/assets/src/scripts/route.tsx @@ -63,7 +63,7 @@ export default [ }, { path: 'admin/reports', - component: () => import('../views/admin/Reports.vue'), + react: () => import('../views/admin/ReportsManagement'), el: '.content > .container-fluid', }, { diff --git a/resources/assets/src/views/admin/Reports.vue b/resources/assets/src/views/admin/Reports.vue deleted file mode 100644 index ebe256ec..00000000 --- a/resources/assets/src/views/admin/Reports.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - diff --git a/resources/assets/src/views/admin/ReportsManagement/ImageBox.module.scss b/resources/assets/src/views/admin/ReportsManagement/ImageBox.module.scss new file mode 100644 index 00000000..37e55cec --- /dev/null +++ b/resources/assets/src/views/admin/ReportsManagement/ImageBox.module.scss @@ -0,0 +1,31 @@ +@use '../../../styles/utils'; + +.box { + width: 240px; + transition-property: box-shadow; + transition-duration: 0.3s; +} + +.body { + flex: unset; + display: flex; + justify-content: center; + + img { + cursor: pointer; + width: 170px; + height: 170px; + } +} + +.footer { + flex: 1 1 auto; + + * { + margin: 2.5px 0; + } +} + +.reason { + @include utils.truncate-text; +} diff --git a/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx b/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx new file mode 100644 index 00000000..07939d61 --- /dev/null +++ b/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx @@ -0,0 +1,116 @@ +import React from 'react' +import { t } from '@/scripts/i18n' +import { Texture } from '@/scripts/types' +import { Report, Status } from './types' +import styles from './ImageBox.module.scss' + +interface Props { + report: Report + onClick(texture: Texture | null): void + onBan(): void + onDelete(): void + onReject(): void +} + +const ImageBox: React.FC = (props) => { + const { report } = props + + const handleImageClick = () => props.onClick(report.texture) + + return ( +
+
+ + {t('skinlib.show.uploader')} + {': '} + + {report.uploaderName} + (UID: {report.uploader}) +
+
+ {report.tid.toString()} +
+
+
+
+ {report.status === Status.Pending ? ( + {t('report.status.0')} + ) : report.status === Status.Resolved ? ( + {t('report.status.1')} + ) : ( + {t('report.status.2')} + )} +
+
+ + +
+
+
+ + {t('report.reporter')} + {': '} + + {report.reporterName} + (UID: {report.reporter}) +
+
+ + + {t('report.reason')} + {': '} + + {report.reason} + +
{report.reason}
+
+ + {t('report.time')} + {': '} + {report.report_at} + +
+
+
+
+ ) +} + +export default ImageBox diff --git a/resources/assets/src/views/admin/ReportsManagement/index.tsx b/resources/assets/src/views/admin/ReportsManagement/index.tsx new file mode 100644 index 00000000..4bc2b087 --- /dev/null +++ b/resources/assets/src/views/admin/ReportsManagement/index.tsx @@ -0,0 +1,155 @@ +import React, { useState, useEffect } from 'react' +import { hot } from 'react-hot-loader/root' +import { useImmer } from 'use-immer' +import { t } from '@/scripts/i18n' +import * as fetch from '@/scripts/net' +import { Paginator, Texture, TextureType } from '@/scripts/types' +import { toast, showModal } from '@/scripts/notify' +import Loading from '@/components/Loading' +import Pagination from '@/components/Pagination' +import ViewerSkeleton from '@/components/ViewerSkeleton' +import { Report, Status } from './types' +import ImageBox from './ImageBox' + +const Previewer = React.lazy(() => import('@/components/Viewer')) + +const ReportsManagement: React.FC = () => { + const [reports, setReports] = useImmer([]) + const [page, setPage] = useState(1) + const [totalPages, setTotalPages] = useState(1) + const [isLoading, setIsLoading] = useState(false) + const [query, setQuery] = useState('status:0 sort:-report_at') + const [viewingTexture, setViewingTexture] = useState(null) + + const getReports = async () => { + setIsLoading(true) + const { data, last_page }: Paginator = await fetch.get( + '/admin/reports/list', + { + q: query, + page, + }, + ) + setTotalPages(last_page) + setReports(() => data) + setIsLoading(false) + } + + useEffect(() => { + getReports() + }, [page]) + + const handleQueryChange = (event: React.ChangeEvent) => { + setQuery(event.target.value) + } + + const handleSubmitQuery = (event: React.FormEvent) => { + event.preventDefault() + getReports() + } + + const handleProceedReport = async ( + report: Report, + index: number, + action: 'ban' | 'delete' | 'reject', + ) => { + type Ok = { code: 0; message: string; data: { status: Status } } + type Err = { code: 1; message: string } + const resp = await fetch.put(`/admin/reports/${report.id}`, { + action, + }) + + if (resp.code === 0) { + toast.success(resp.message) + setReports((reports) => { + reports[index].status = resp.data.status + }) + } else { + toast.error(resp.message) + } + } + + const handleDelete = async (report: Report, index: number) => { + try { + await showModal({ + text: t('skinlib.deleteNotice'), + okButtonType: 'danger', + }) + } catch { + return + } + + handleProceedReport(report, index, 'delete') + } + + const textureUrl = + viewingTexture && `${blessing.base_url}/textures/${viewingTexture.hash}` + + return ( +
+
+
+
+
+ +
+ +
+
+
+ {isLoading ? ( +
+ +
+ ) : reports.length === 0 ? ( +
{t('general.noResult')}
+ ) : ( +
+ {reports.map((report, i) => ( + handleProceedReport(report, i, 'ban')} + onDelete={() => handleDelete(report, i)} + onReject={() => handleProceedReport(report, i, 'reject')} + /> + ))} +
+ )} +
+
+ +
+
+
+
+
+ }> + + +
+
+ ) +} + +export default hot(ReportsManagement) diff --git a/resources/assets/src/views/admin/ReportsManagement/types.ts b/resources/assets/src/views/admin/ReportsManagement/types.ts new file mode 100644 index 00000000..b6edba80 --- /dev/null +++ b/resources/assets/src/views/admin/ReportsManagement/types.ts @@ -0,0 +1,20 @@ +import { Texture } from '@/scripts/types' + +export const enum Status { + Pending = 0, + Resolved = 1, + Rejected = 2, +} + +export type Report = { + id: number + tid: number + texture: Texture | null + uploader: number + uploaderName: string + reporter: number + reporterName: string + reason: string + status: Status + report_at: string +} diff --git a/resources/assets/tests/views/admin/Reports.test.ts b/resources/assets/tests/views/admin/Reports.test.ts deleted file mode 100644 index 98e9c41c..00000000 --- a/resources/assets/tests/views/admin/Reports.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import { flushPromises } from '../../utils' -import { toast } from '@/scripts/notify' -import Reports from '@/views/admin/Reports.vue' - -jest.mock('@/scripts/notify') - -test('basic render', async () => { - Vue.prototype.$http.get.mockResolvedValue({ - data: [{ - id: 1, - uploader: 1, - uploaderName: 'a', - reporter: 2, - reporterName: 'b', - reason: 'sth', - status: 0, - }], - }) - const wrapper = mount(Reports) - await flushPromises() - const text = wrapper.text() - expect(text).toContain('a (UID: 1)') - expect(text).toContain('b (UID: 2)') - expect(text).toContain('sth') - expect(text).toContain('report.status.0') -}) - -test('link to skin library', async () => { - Vue.prototype.$http.get.mockResolvedValue({ - data: [{ id: 1, tid: 1 }], - }) - const wrapper = mount(Reports) - await flushPromises() - expect(wrapper.find('a').attributes('href')).toBe('/skinlib/show/1') -}) - -test('delete texture', async () => { - Vue.prototype.$http.get.mockResolvedValue({ data: [{ id: 1, status: 0 }] }) - Vue.prototype.$http.post - .mockResolvedValueOnce({ code: 1, message: 'fail' }) - .mockResolvedValue({ - code: 0, message: 'ok', data: { status: 1 }, - }) - const wrapper = mount(Reports) - await flushPromises() - const button = wrapper.findAll('a').at(1) - - button.trigger('click') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/admin/reports', - { id: 1, action: 'delete' }, - ) - expect(toast.error).toBeCalledWith('fail') - - button.trigger('click') - await flushPromises() - expect(toast.success).toBeCalledWith('ok') - expect(wrapper.text()).toContain('report.status.1') -}) - -test('ban uploader', async () => { - Vue.prototype.$http.get.mockResolvedValue({ data: [{ id: 1, status: 0 }] }) - Vue.prototype.$http.post - .mockResolvedValue({ - code: 0, message: 'ok', data: { status: 1 }, - }) - const wrapper = mount(Reports) - await flushPromises() - const button = wrapper.findAll('a').at(2) - - button.trigger('click') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/admin/reports', - { id: 1, action: 'ban' }, - ) - expect(toast.success).toBeCalledWith('ok') - expect(wrapper.text()).toContain('report.status.1') -}) - -test('reject', async () => { - Vue.prototype.$http.get.mockResolvedValue({ data: [{ id: 1, status: 0 }] }) - Vue.prototype.$http.post - .mockResolvedValue({ - code: 0, message: 'ok', data: { status: 2 }, - }) - const wrapper = mount(Reports) - await flushPromises() - const button = wrapper.find('button') - - button.trigger('click') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/admin/reports', - { id: 1, action: 'reject' }, - ) - expect(toast.success).toBeCalledWith('ok') - expect(wrapper.text()).toContain('report.status.2') -}) diff --git a/resources/assets/tests/views/admin/ReportsManagement.test.tsx b/resources/assets/tests/views/admin/ReportsManagement.test.tsx new file mode 100644 index 00000000..3f43cd06 --- /dev/null +++ b/resources/assets/tests/views/admin/ReportsManagement.test.tsx @@ -0,0 +1,172 @@ +import React from 'react' +import { render, waitFor, fireEvent } from '@testing-library/react' +import { createPaginator } from '../../utils' +import { t } from '@/scripts/i18n' +import * as fetch from '@/scripts/net' +import { Texture, TextureType } from '@/scripts/types' +import ReportsManagement from '@/views/admin/ReportsManagement' +import { Report, Status } from '@/views/admin/ReportsManagement/types' + +jest.mock('@/scripts/net') + +const fixture: Readonly = Object.freeze({ + id: 1, + tid: 1, + texture: null, + uploader: 1, + uploaderName: 'xx', + reporter: 2, + reporterName: 'yy', + reason: 'nsfw', + status: Status.Pending, + report_at: new Date().toString(), +}) + +test('search reports', async () => { + fetch.get.mockResolvedValue(createPaginator([])) + + const { getByTitle, getByText } = render() + await waitFor(() => + expect(fetch.get).toBeCalledWith('/admin/reports/list', { + q: 'status:0 sort:-report_at', + page: 1, + }), + ) + + fireEvent.input(getByTitle(t('vendor.datatable.search')), { + target: { value: 's' }, + }) + fireEvent.click(getByText(t('vendor.datatable.search'))) + await waitFor(() => + expect(fetch.get).toBeCalledWith('/admin/reports/list', { + q: 's', + page: 1, + }), + ) +}) + +test('preview texture', async () => { + const texture: Texture = { + tid: fixture.tid, + name: 'cape', + type: TextureType.Cape, + hash: 'def', + size: 2, + uploader: fixture.uploader, + public: true, + upload_at: new Date().toString(), + likes: 1, + } + fetch.get.mockResolvedValue(createPaginator([{ ...fixture, texture }])) + + const { getByAltText } = render() + await waitFor(() => expect(fetch.get).toBeCalled()) + + fireEvent.click(getByAltText(fixture.tid.toString())) +}) + +describe('proceed report', () => { + beforeEach(() => { + fetch.get.mockResolvedValue(createPaginator([fixture])) + }) + + describe('ban uploader', () => { + it('succeeded', async () => { + fetch.put.mockResolvedValue({ + code: 0, + message: 'ok', + data: { status: Status.Resolved }, + }) + + const { getByText, getByRole, queryByText } = render( + , + ) + await waitFor(() => expect(fetch.get).toBeCalled()) + + fireEvent.click(getByText(t('report.ban'))) + await waitFor(() => + expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, { + action: 'ban', + }), + ) + expect(queryByText('ok')).toBeInTheDocument() + expect(getByRole('status')).toBeInTheDocument() + expect(queryByText(t('report.status.1'))).toBeInTheDocument() + }) + + it('failed', async () => { + fetch.put.mockResolvedValue({ code: 1, message: 'failed' }) + + const { getByText, getByRole, queryByText } = render( + , + ) + await waitFor(() => expect(fetch.get).toBeCalled()) + + fireEvent.click(getByText(t('report.ban'))) + await waitFor(() => + expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, { + action: 'ban', + }), + ) + expect(queryByText('failed')).toBeInTheDocument() + expect(getByRole('alert')).toBeInTheDocument() + expect(queryByText(t('report.status.0'))).toBeInTheDocument() + }) + }) + + describe('delete texture', () => { + it('cancelled', async () => { + const { getByText } = render() + await waitFor(() => expect(fetch.get).toBeCalled()) + + fireEvent.click(getByText(t('skinlib.show.delete-texture'))) + fireEvent.click(getByText(t('general.cancel'))) + expect(fetch.put).not.toBeCalled() + }) + + it('succeeded', async () => { + fetch.put.mockResolvedValue({ + code: 0, + message: 'ok', + data: { status: Status.Resolved }, + }) + + const { getByText, getByRole, queryByText } = render( + , + ) + await waitFor(() => expect(fetch.get).toBeCalled()) + + fireEvent.click(getByText(t('skinlib.show.delete-texture'))) + fireEvent.click(getByText(t('general.confirm'))) + await waitFor(() => + expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, { + action: 'delete', + }), + ) + expect(queryByText('ok')).toBeInTheDocument() + expect(getByRole('status')).toBeInTheDocument() + expect(queryByText(t('report.status.1'))).toBeInTheDocument() + }) + }) + + it('reject report', async () => { + fetch.put.mockResolvedValue({ + code: 0, + message: 'ok', + data: { status: Status.Rejected }, + }) + + const { getByText, getByRole, queryByText } = render() + await waitFor(() => expect(fetch.get).toBeCalled()) + + fireEvent.click(getByText(t('report.reject'))) + await waitFor(() => + expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, { + action: 'reject', + }), + ) + expect(queryByText('ok')).toBeInTheDocument() + expect(getByRole('status')).toBeInTheDocument() + expect(queryByText(t('report.status.2'))).toBeInTheDocument() + }) +}) diff --git a/resources/misc/changelogs/en/5.0.0.md b/resources/misc/changelogs/en/5.0.0.md index 45b87f3a..7e26ab5e 100644 --- a/resources/misc/changelogs/en/5.0.0.md +++ b/resources/misc/changelogs/en/5.0.0.md @@ -73,6 +73,7 @@ - Fixed potential "Invalid Signature" issue. - Fixed that duplicated player name is not detected when updating player name in administration panel. - Fixed that normal administrator can set other user as administrator. +- Fixed that texture file won't be deleted when deleting texture in reports management. ## Removed diff --git a/resources/misc/changelogs/zh_CN/5.0.0.md b/resources/misc/changelogs/zh_CN/5.0.0.md index 35e422ba..1b331bd5 100644 --- a/resources/misc/changelogs/zh_CN/5.0.0.md +++ b/resources/misc/changelogs/zh_CN/5.0.0.md @@ -73,6 +73,7 @@ - 修复可能的「Invalid Signature」问题 - 修复在管理面板中修改角色名时不检测角色名是否重复的问题 - 修复普通管理员可设置其他用户为管理员的问题 +- 修复处理举报中删除材质时不删除材质文件的问题 ## 移除 diff --git a/routes/api.php b/routes/api.php index 51635150..d6b68251 100644 --- a/routes/api.php +++ b/routes/api.php @@ -61,5 +61,10 @@ Route::prefix('admin') Route::delete('{uid}', 'ClosetManagementController@remove'); }); + Route::prefix('reports')->group(function () { + Route::get('', 'ReportController@manage'); + Route::put('{report}', 'ReportController@review'); + }); + Route::post('notifications', 'NotificationsController@send'); }); diff --git a/routes/web.php b/routes/web.php index d30e0ff9..dba7e2e2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -159,8 +159,8 @@ Route::prefix('admin') Route::prefix('reports')->group(function () { Route::view('', 'admin.reports'); - Route::post('', 'ReportController@review'); - Route::any('list', 'ReportController@manage'); + Route::put('{report}', 'ReportController@review'); + Route::get('list', 'ReportController@manage'); }); Route::prefix('i18n')->group(function () { diff --git a/tests/HttpTest/ControllersTest/ReportControllerTest.php b/tests/HttpTest/ControllersTest/ReportControllerTest.php index 49bdbef2..9294622a 100644 --- a/tests/HttpTest/ControllersTest/ReportControllerTest.php +++ b/tests/HttpTest/ControllersTest/ReportControllerTest.php @@ -9,6 +9,7 @@ use Blessing\Filter; use Blessing\Rejection; use Event; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Storage; class ReportControllerTest extends TestCase { @@ -22,20 +23,20 @@ class ReportControllerTest extends TestCase $user = factory(User::class)->create(); $texture = factory(Texture::class)->create(); - // Without `tid` field + // without `tid` field $this->actingAs($user) ->postJson('/skinlib/report') ->assertJsonValidationErrors('tid'); - // Invalid texture + // invalid texture $this->postJson('/skinlib/report', ['tid' => $texture->tid - 1]) ->assertJsonValidationErrors('tid'); - // Without `reason` field + // without `reason` field $this->postJson('/skinlib/report', ['tid' => $texture->tid]) ->assertJsonValidationErrors('reason'); - // Lack of score + // lack of score $user->score = 0; $user->save(); option(['reporter_score_modification' => -5]); @@ -46,7 +47,7 @@ class ReportControllerTest extends TestCase ]); option(['reporter_score_modification' => 5]); - // Rejection + // rejection $filter->add( 'user_can_report', function ($can, $tid, $reason, $reporter) use ($texture, $user) { @@ -61,7 +62,7 @@ class ReportControllerTest extends TestCase ->assertJson(['code' => 1, 'message' => 'rejected']); $filter->remove('user_can_report'); - // Success + // success $this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason']) ->assertJson([ 'code' => 0, @@ -93,7 +94,7 @@ class ReportControllerTest extends TestCase return true; }); - // Prevent duplication + // prevent duplication $this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason']) ->assertJson([ 'code' => 1, @@ -134,18 +135,7 @@ class ReportControllerTest extends TestCase $this->actingAs($reporter) ->getJson('/admin/reports/list') - ->assertJson([ - 'totalRecords' => 1, - 'data' => [[ - 'tid' => $texture->tid, - 'uploader' => $uploader->uid, - 'reporter' => $reporter->uid, - 'reason' => 'test', - 'status' => Report::PENDING, - 'uploaderName' => $uploader->nickname, - 'reporterName' => $reporter->nickname, - ]], - ]); + ->assertJson(['data' => [$report->toArray()]]); } public function testReview() @@ -164,25 +154,17 @@ class ReportControllerTest extends TestCase $report->save(); $report->refresh(); - // Without `id` field - $this->actingAs($admin) - ->postJson('/admin/reports') - ->assertJsonValidationErrors('id'); - - // Not existed - $this->postJson('/admin/reports', ['id' => $report->id - 1]) - ->assertJsonValidationErrors('id'); - // Without `action` field - $this->postJson('/admin/reports', ['id' => $report->id]) + $this->actingAs($admin) + ->putJson('/admin/reports/'.$report->id) ->assertJsonValidationErrors('action'); - // Invalid action - $this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'a']) + // invalid action + $this->putJson('/admin/reports/'.$report->id, ['action' => 'a']) ->assertJsonValidationErrors('action'); - // Allow to process again - $this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject']) + // allow to process again + $this->putJson('/admin/reports/'.$report->id, ['action' => 'reject']) ->assertJson(['code' => 0]); $id = $report->id; Event::assertDispatched('report.reviewing', function ($event, $payload) use ($id) { @@ -213,10 +195,10 @@ class ReportControllerTest extends TestCase $report->refresh(); $id = $report->id; - // Should not cost score + // should not cost score $score = $reporter->score; $this->actingAs($admin) - ->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject']) + ->putJson('/admin/reports/'.$report->id, ['action' => 'reject']) ->assertJson([ 'code' => 0, 'message' => trans('general.op-success'), @@ -240,12 +222,12 @@ class ReportControllerTest extends TestCase return true; }); - // Should cost score + // should cost score $report->status = Report::PENDING; $report->save(); option(['reporter_score_modification' => 5]); $score = $reporter->score; - $this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject']) + $this->putJson('/admin/reports/'.$report->id, ['action' => 'reject']) ->assertJson(['code' => 0]); $reporter->refresh(); $this->assertEquals($score - 5, $reporter->score); @@ -254,11 +236,13 @@ class ReportControllerTest extends TestCase public function testReviewDelete() { Event::fake(); + $disk = Storage::fake('textures'); $uploader = factory(User::class)->create(); $reporter = factory(User::class)->create(); $admin = factory(User::class)->states('admin')->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + $disk->put($texture->hash, ''); $report = new Report(); $report->tid = $texture->tid; @@ -278,7 +262,7 @@ class ReportControllerTest extends TestCase ]); $score = $reporter->score; $this->actingAs($admin) - ->postJson('/admin/reports', ['id' => $report->id, 'action' => 'delete']) + ->putJson('/admin/reports/'.$report->id, ['action' => 'delete']) ->assertJson([ 'code' => 0, 'message' => trans('general.op-success'), @@ -289,6 +273,7 @@ class ReportControllerTest extends TestCase $this->assertEquals(Report::RESOLVED, $report->status); $this->assertNull(Texture::find($texture->tid)); $this->assertEquals($score + 7, $reporter->score); + Storage::assertMissing($texture->hash); Event::assertDispatched('report.reviewing', function ($event, $payload) use ($id) { [$report, $action] = $payload; $this->assertEquals($id, $report->id); @@ -337,7 +322,7 @@ class ReportControllerTest extends TestCase $score = $reporter->score; $texture->delete(); $this->actingAs($admin) - ->postJson('/admin/reports', ['id' => $report->id, 'action' => 'delete']) + ->putJson('/admin/reports/'.$report->id, ['action' => 'delete']) ->assertJson([ 'code' => 0, 'message' => trans('general.texture-deleted'), @@ -375,11 +360,11 @@ class ReportControllerTest extends TestCase $report->refresh(); $id = $report->id; - // Uploader should be banned + // uploader should be banned option(['reporter_reward_score' => 6]); $score = $reporter->score; $this->actingAs($admin) - ->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban']) + ->putJson('/admin/reports/'.$report->id, ['action' => 'ban']) ->assertJson([ 'code' => 0, 'message' => trans('general.op-success'), @@ -391,14 +376,14 @@ class ReportControllerTest extends TestCase $this->assertEquals($score + 6, $reporter->score); option(['reporter_reward_score' => 0]); - // Should not ban admin uploader + // should not ban admin uploader $report->refresh(); $report->status = Report::PENDING; $report->save(); $uploader->refresh(); $uploader->permission = User::ADMIN; $uploader->save(); - $this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban']) + $this->putJson('/admin/reports/'.$report->id, ['action' => 'ban']) ->assertJson([ 'code' => 1, 'message' => trans('admin.users.operations.no-permission'), @@ -427,10 +412,10 @@ class ReportControllerTest extends TestCase return true; }); - // Uploader has deleted its account + // uploader has deleted its account $report->uploader = -1; $report->save(); - $this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban']) + $this->putJson('/admin/reports/'.$report->id, ['action' => 'ban']) ->assertJson([ 'code' => 1, 'message' => trans('admin.users.operations.non-existent'),