From 946896117a70567e213327197a82c4f01786ada0 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 5 Jun 2020 23:35:49 +0800 Subject: [PATCH] refactor `SkinlibController` --- app/Http/Controllers/SkinlibController.php | 141 +++---- resources/assets/src/scripts/urls.ts | 27 +- .../assets/src/views/skinlib/Show/index.tsx | 31 +- resources/assets/src/views/skinlib/Upload.tsx | 2 +- .../assets/tests/views/skinlib/Show.test.tsx | 64 ++-- .../tests/views/skinlib/Upload.test.tsx | 2 +- routes/web.php | 22 +- scripts/generateUrls.ts | 30 +- .../ControllersTest/SkinlibControllerTest.php | 360 +++++++----------- 9 files changed, 278 insertions(+), 401 deletions(-) diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php index 4ccb4014..7b19aac6 100644 --- a/app/Http/Controllers/SkinlibController.php +++ b/app/Http/Controllers/SkinlibController.php @@ -19,6 +19,23 @@ use Storage; class SkinlibController extends Controller { + public function __construct() + { + $this->middleware(function (Request $request, $next) { + /** @var User */ + $user = $request->user(); + /** @var Texture */ + $texture = $request->route('texture'); + + if ($texture->uploader != $user->uid && !$user->isAdmin()) { + return json(trans('skinlib.no-permission'), 1) + ->setStatusCode(403); + } + + return $next($request); + })->only(['rename', 'privacy', 'type', 'delete']); + } + public function library(Request $request) { $user = Auth::user(); @@ -285,22 +302,10 @@ class SkinlibController extends Controller ]); } - public function delete(Request $request) + public function delete(Texture $texture) { - $texture = Texture::find($request->tid); - /** @var User */ - $user = Auth::user(); - - if (!$texture) { - return json(trans('skinlib.non-existent'), 1); - } - - if ($texture->uploader != $user->uid && !$user->isAdmin()) { - return json(trans('skinlib.no-permission'), 1); - } - // check if file occupied - if (Texture::where('hash', $texture->hash)->count() == 1) { + if (Texture::where('hash', $texture->hash)->count() === 1) { Storage::disk('textures')->delete($texture->hash); } @@ -309,101 +314,75 @@ class SkinlibController extends Controller return json(trans('skinlib.delete.success'), 0); } - public function privacy(Request $request) + public function privacy(Texture $texture) { - $t = Texture::find($request->input('tid')); - $user = $request->user(); - - if (!$t) { - return json(trans('skinlib.non-existent'), 1); - } - - if ($t->uploader != $user->uid && !$user->isAdmin()) { - return json(trans('skinlib.no-permission'), 1); - } - - $uploader = User::find($t->uploader); - $score_diff = $t->size * (option('private_score_per_storage') - option('score_per_storage')) * ($t->public ? -1 : 1); - if ($t->public && option('take_back_scores_after_deletion', true)) { + $uploader = User::find($texture->uploader); + $score_diff = $texture->size + * (option('private_score_per_storage') - option('score_per_storage')) + * ($texture->public ? -1 : 1); + if ($texture->public && option('take_back_scores_after_deletion', true)) { $score_diff -= option('score_award_per_texture', 0); } if ($uploader->score + $score_diff < 0) { return json(trans('skinlib.upload.lack-score'), 1); } - $type = $t->type == 'cape' ? 'cape' : 'skin'; - Player::where("tid_$type", $t->tid) + $type = $texture->type == 'cape' ? 'cape' : 'skin'; + Player::where("tid_$type", $texture->tid) ->where('uid', '<>', session('uid')) ->update(["tid_$type" => 0]); - $t->likers()->get()->each(function ($user) use ($t) { - $user->closet()->detach($t->tid); + $texture->likers()->get()->each(function ($user) use ($texture) { + $user->closet()->detach($texture->tid); if (option('return_score')) { $user->score += option('score_per_closet_item'); $user->save(); } - $t->likes--; + $texture->likes--; }); $uploader->score += $score_diff; $uploader->save(); - $t->public = !$t->public; - $t->save(); + $texture->public = !$texture->public; + $texture->save(); - return json( - trans('skinlib.privacy.success', ['privacy' => (!$t->public ? trans('general.private') : trans('general.public'))]), - 0 - ); - } - - public function rename(Request $request) - { - $request->validate([ - 'tid' => 'required|integer', - 'new_name' => 'required', + $message = trans('skinlib.privacy.success', [ + 'privacy' => ( + $texture->public + ? trans('general.public') + : trans('general.private')), ]); - $user = $request->user(); - $t = Texture::find($request->input('tid')); - if (!$t) { - return json(trans('skinlib.non-existent'), 1); - } - - if ($t->uploader != $user->uid && !$user->isAdmin()) { - return json(trans('skinlib.no-permission'), 1); - } - - $t->name = $request->input('new_name'); - - if ($t->save()) { - return json(trans('skinlib.rename.success', ['name' => $request->input('new_name')]), 0); - } + return json($message, 0); } - // @codeCoverageIgnore - - public function model(Request $request) + public function rename(Request $request, Texture $texture) + { + $data = $request->validate(['name' => [ + 'required', + option('texture_name_regexp') + ? 'regex:'.option('texture_name_regexp') + : 'string', + ]]); + $name = $data['name']; + + $texture->name = $name; + $texture->save(); + + return json(trans('skinlib.rename.success', ['name' => $name]), 0); + } + + public function type(Request $request, Texture $texture) { - $user = $request->user(); $data = $request->validate([ - 'tid' => 'required|integer', - 'model' => 'required|in:steve,alex,cape', + 'type' => ['required', Rule::in(['steve', 'alex', 'cape'])], ]); + $type = $data['type']; - $t = Texture::find($request->input('tid')); + $texture->type = $type; + $texture->save(); - if (!$t) { - return json(trans('skinlib.non-existent'), 1); - } - - if ($t->uploader != $user->uid && !$user->isAdmin()) { - return json(trans('skinlib.no-permission'), 1); - } - - $t->type = $request->input('model'); - $t->save(); - - return json(trans('skinlib.model.success', ['model' => $data['model']]), 0); + return json(trans('skinlib.model.success', ['model' => $type]), 0); } } diff --git a/resources/assets/src/scripts/urls.ts b/resources/assets/src/scripts/urls.ts index 03ae2765..9bae27d6 100644 --- a/resources/assets/src/scripts/urls.ts +++ b/resources/assets/src/scripts/urls.ts @@ -1,16 +1,16 @@ export default { admin: { players: { - list: () => '/admin/players/list' as const, delete: (player: number) => `/admin/players/${player}`, + list: () => '/admin/players/list' as const, name: (player: number) => `/admin/players/${player}/name`, owner: (player: number) => `/admin/players/${player}/owner`, texture: (player: number) => `/admin/players/${player}/textures`, }, users: { - list: () => '/admin/users/list' as const, delete: (user: number) => `/admin/users/${user}`, email: (user: number) => `/admin/users/${user}/email`, + list: () => '/admin/users/list' as const, nickname: (user: number) => `/admin/users/${user}/nickname`, password: (user: number) => `/admin/users/${user}/password`, permission: (user: number) => `/admin/users/${user}/permission`, @@ -32,26 +32,33 @@ export default { info: (texture: number) => `/skinlib/info/${texture}`, list: () => '/skinlib/list' as const, show: (tid: number) => `/skinlib/show/${tid}`, - upload: () => '/skinlib/upload' as const, + }, + texture: { + delete: (texture: number) => `/texture/${texture}`, + info: (texture: number) => `/texture/${texture}`, + name: (texture: number) => `/texture/${texture}/name`, + privacy: (texture: number) => `/texture/${texture}/privacy`, + type: (texture: number) => `/texture/${texture}/type`, + upload: () => '/texture' as const, }, user: { - home: () => '/user' as const, closet: { add: () => '/user/closet' as const, - page: () => '/user/closet' as const, ids: () => '/user/closet/ids' as const, list: () => '/user/closet/list' as const, - rename: (tid: number) => `/user/closet/${tid}`, + page: () => '/user/closet' as const, remove: (tid: number) => `/user/closet/${tid}`, + rename: (tid: number) => `/user/closet/${tid}`, }, + home: () => '/user' as const, notification: (id: number) => `/user/notifications/${id}`, player: { - page: () => '/user/player' as const, add: () => '/user/player' as const, - list: () => '/user/player/list' as const, - delete: (player: number) => `/user/player/${player}`, - rename: (player: number) => `/user/player/${player}/name`, clear: (player: number) => `/user/player/${player}/textures`, + delete: (player: number) => `/user/player/${player}`, + list: () => '/user/player/list' as const, + page: () => '/user/player' as const, + rename: (player: number) => `/user/player/${player}/name`, set: (player: number) => `/user/player/${player}/textures`, }, profile: { avatar: () => '/user/profile/avatar' as const }, diff --git a/resources/assets/src/views/skinlib/Show/index.tsx b/resources/assets/src/views/skinlib/Show/index.tsx index 96a67049..5456361a 100644 --- a/resources/assets/src/views/skinlib/Show/index.tsx +++ b/resources/assets/src/views/skinlib/Show/index.tsx @@ -7,6 +7,7 @@ import { t } from '@/scripts/i18n' import * as fetch from '@/scripts/net' import { showModal, toast } from '@/scripts/notify' import { Texture, TextureType } from '@/scripts/types' +import urls from '@/scripts/urls' import ButtonEdit from '@/components/ButtonEdit' import ViewerSkeleton from '@/components/ViewerSkeleton' import ModalApply from '@/views/user/Closet/ModalApply' @@ -38,7 +39,7 @@ const Show: React.FC = () => { const fetchInfo = async () => { const url = location.href .replace(blessing.base_url, '') - .replace('show', 'info') + .replace('skinlib/show', 'texture') const texture = await fetch.get(url) setTexture(texture) @@ -68,12 +69,9 @@ const Show: React.FC = () => { return } - const { code, message } = await fetch.post( - '/skinlib/rename', - { - tid: texture.tid, - new_name: name, - }, + const { code, message } = await fetch.put( + urls.texture.name(texture.tid), + { name }, ) if (code === 0) { toast.success(message) @@ -102,12 +100,9 @@ const Show: React.FC = () => { return } - const { code, message } = await fetch.post( - '/skinlib/model', - { - tid: texture.tid, - model: type, - }, + const { code, message } = await fetch.put( + urls.texture.type(texture.tid), + { type }, ) if (code === 0) { toast.success(message) @@ -190,9 +185,8 @@ const Show: React.FC = () => { return } - const { code, message } = await fetch.post( - '/skinlib/privacy', - { tid: texture.tid }, + const { code, message } = await fetch.put( + urls.texture.privacy(texture.tid), ) if (code === 0) { toast.success(message) @@ -212,9 +206,8 @@ const Show: React.FC = () => { return } - const { code, message } = await fetch.post( - '/skinlib/delete', - { tid: texture.tid }, + const { code, message } = await fetch.del( + urls.texture.delete(texture.tid), ) if (code === 0) { toast.success(message) diff --git a/resources/assets/src/views/skinlib/Upload.tsx b/resources/assets/src/views/skinlib/Upload.tsx index d44116ee..5c7bbf23 100644 --- a/resources/assets/src/views/skinlib/Upload.tsx +++ b/resources/assets/src/views/skinlib/Upload.tsx @@ -87,7 +87,7 @@ const Upload: React.FC = () => { setIsUploading(true) const { code, message, data: { tid } = { tid: 0 } } = await fetch.post< fetch.ResponseBody<{ tid: number }> - >(urls.skinlib.upload(), formData) + >(urls.texture.upload(), formData) setIsUploading(false) if (code === 0) { diff --git a/resources/assets/tests/views/skinlib/Show.test.tsx b/resources/assets/tests/views/skinlib/Show.test.tsx index 8abad3a4..c3fed242 100644 --- a/resources/assets/tests/views/skinlib/Show.test.tsx +++ b/resources/assets/tests/views/skinlib/Show.test.tsx @@ -113,9 +113,7 @@ test('badges', async () => { test('apply to player', async () => { window.blessing.extra.currentUid = 2 window.blessing.extra.inCloset = true - fetch.get - .mockResolvedValueOnce(fixtureSkin) - .mockResolvedValueOnce([]) + fetch.get.mockResolvedValueOnce(fixtureSkin).mockResolvedValueOnce([]) const { getByText, getByLabelText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) @@ -209,12 +207,12 @@ describe('edit texture name', () => { expect(queryByText(t('skinlib.emptyNewTextureName'))).toBeInTheDocument() fireEvent.click(getByText(t('general.cancel'))) - await waitFor(() => expect(fetch.post).not.toBeCalled()) + await waitFor(() => expect(fetch.put).not.toBeCalled()) expect(queryByText(fixtureSkin.name)).toBeInTheDocument() }) it('succeeded', async () => { - fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + fetch.put.mockResolvedValue({ code: 0, message: 'ok' }) const { getByText, @@ -231,9 +229,8 @@ describe('edit texture name', () => { }) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/rename', { - tid: fixtureSkin.tid, - new_name: 't', + expect(fetch.put).toBeCalledWith(urls.texture.name(fixtureSkin.tid), { + name: 't', }), ) expect(queryByText('ok')).toBeInTheDocument() @@ -242,7 +239,7 @@ describe('edit texture name', () => { }) it('failed', async () => { - fetch.post.mockResolvedValue({ code: 1, message: 'failed' }) + fetch.put.mockResolvedValue({ code: 1, message: 'failed' }) const { getByText, @@ -259,9 +256,8 @@ describe('edit texture name', () => { }) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/rename', { - tid: fixtureSkin.tid, - new_name: 't', + expect(fetch.put).toBeCalledWith(urls.texture.name(fixtureSkin.tid), { + name: 't', }), ) expect(queryByText('failed')).toBeInTheDocument() @@ -285,12 +281,12 @@ describe('edit texture type', () => { fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1]) fireEvent.click(getByLabelText('Alex')) fireEvent.click(getByText(t('general.cancel'))) - await waitFor(() => expect(fetch.post).not.toBeCalled()) + await waitFor(() => expect(fetch.put).not.toBeCalled()) expect(queryByText('steve')).toBeInTheDocument() }) it('succeeded', async () => { - fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + fetch.put.mockResolvedValue({ code: 0, message: 'ok' }) const { getByText, @@ -305,9 +301,8 @@ describe('edit texture type', () => { fireEvent.click(getByLabelText('Alex')) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/model', { - tid: fixtureSkin.tid, - model: 'alex', + expect(fetch.put).toBeCalledWith(urls.texture.type(fixtureSkin.tid), { + type: 'alex', }), ) expect(queryByText('ok')).toBeInTheDocument() @@ -316,7 +311,7 @@ describe('edit texture type', () => { }) it('failed', async () => { - fetch.post.mockResolvedValue({ code: 1, message: 'failed' }) + fetch.put.mockResolvedValue({ code: 1, message: 'failed' }) const { getByText, @@ -331,9 +326,8 @@ describe('edit texture type', () => { fireEvent.click(getByLabelText('Alex')) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/model', { - tid: fixtureSkin.tid, - model: 'alex', + expect(fetch.put).toBeCalledWith(urls.texture.type(fixtureSkin.tid), { + type: 'alex', }), ) expect(queryByText('failed')).toBeInTheDocument() @@ -544,13 +538,13 @@ describe('change privacy', () => { fireEvent.click(getByText(t('skinlib.setAsPrivate'))) fireEvent.click(getByText(t('general.cancel'))) - await waitFor(() => expect(fetch.post).not.toBeCalled()) + await waitFor(() => expect(fetch.put).not.toBeCalled()) expect(queryByText(t('skinlib.setAsPrivate'))).toBeInTheDocument() }) it('succeeded', async () => { fetch.get.mockResolvedValue(fixtureSkin) - fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + fetch.put.mockResolvedValue({ code: 0, message: 'ok' }) const { getByText, getByRole, queryByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) @@ -558,9 +552,7 @@ describe('change privacy', () => { fireEvent.click(getByText(t('skinlib.setAsPrivate'))) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/privacy', { - tid: fixtureSkin.tid, - }), + expect(fetch.put).toBeCalledWith(urls.texture.privacy(fixtureSkin.tid)), ) expect(queryByText('ok')).toBeInTheDocument() expect(getByRole('status')).toHaveClass('alert-success') @@ -569,7 +561,7 @@ describe('change privacy', () => { it('failed', async () => { fetch.get.mockResolvedValue({ ...fixtureSkin, public: false }) - fetch.post.mockResolvedValue({ code: 1, message: 'failed' }) + fetch.put.mockResolvedValue({ code: 1, message: 'failed' }) const { getByText, getByRole, queryByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) @@ -577,9 +569,7 @@ describe('change privacy', () => { fireEvent.click(getByText(t('skinlib.setAsPublic'))) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/privacy', { - tid: fixtureSkin.tid, - }), + expect(fetch.put).toBeCalledWith(urls.texture.privacy(fixtureSkin.tid)), ) expect(queryByText('failed')).toBeInTheDocument() expect(getByRole('alert')).toHaveClass('alert-danger') @@ -599,11 +589,11 @@ describe('delete texture', () => { fireEvent.click(getByText(t('skinlib.show.delete-texture'))) fireEvent.click(getByText(t('general.cancel'))) - await waitFor(() => expect(fetch.post).not.toBeCalled()) + await waitFor(() => expect(fetch.del).not.toBeCalled()) }) it('succeeded', async () => { - fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + fetch.del.mockResolvedValue({ code: 0, message: 'ok' }) const { getByText, getByRole, queryByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) @@ -611,9 +601,7 @@ describe('delete texture', () => { fireEvent.click(getByText(t('skinlib.show.delete-texture'))) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/delete', { - tid: fixtureSkin.tid, - }), + expect(fetch.del).toBeCalledWith(urls.texture.delete(fixtureSkin.tid)), ) expect(queryByText('ok')).toBeInTheDocument() expect(getByRole('status')).toHaveClass('alert-success') @@ -622,7 +610,7 @@ describe('delete texture', () => { }) it('failed', async () => { - fetch.post.mockResolvedValue({ code: 1, message: 'failed' }) + fetch.del.mockResolvedValue({ code: 1, message: 'failed' }) const { getByText, getByRole, queryByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) @@ -630,9 +618,7 @@ describe('delete texture', () => { fireEvent.click(getByText(t('skinlib.show.delete-texture'))) fireEvent.click(getByText(t('general.confirm'))) await waitFor(() => - expect(fetch.post).toBeCalledWith('/skinlib/delete', { - tid: fixtureSkin.tid, - }), + expect(fetch.del).toBeCalledWith(urls.texture.delete(fixtureSkin.tid)), ) expect(queryByText('failed')).toBeInTheDocument() expect(getByRole('alert')).toHaveClass('alert-danger') diff --git a/resources/assets/tests/views/skinlib/Upload.test.tsx b/resources/assets/tests/views/skinlib/Upload.test.tsx index 5f4b0abf..c35aa1ff 100644 --- a/resources/assets/tests/views/skinlib/Upload.test.tsx +++ b/resources/assets/tests/views/skinlib/Upload.test.tsx @@ -215,7 +215,7 @@ describe('upload texture', () => { expect(queryByText(t('skinlib.uploading'))).toBeInTheDocument() expect(fetch.post).toBeCalledWith( - urls.skinlib.upload(), + urls.texture.upload(), expect.any(FormData), ) diff --git a/routes/web.php b/routes/web.php index f47d8b79..dbd97959 100644 --- a/routes/web.php +++ b/routes/web.php @@ -91,6 +91,19 @@ Route::prefix('user') Route::view('oauth/manage', 'user.oauth')->middleware('verified'); }); +Route::prefix('texture')->name('texture.')->group(function () { + Route::get('{texture}', 'SkinlibController@info')->name('info'); + Route::middleware(['authorize', 'verified'])->group(function () { + Route::post('', 'SkinlibController@handleUpload')->name('upload'); + Route::prefix('{texture}')->group(function () { + Route::put('type', 'SkinlibController@type')->name('type'); + Route::put('name', 'SkinlibController@rename')->name('name'); + Route::put('privacy', 'SkinlibController@privacy')->name('privacy'); + Route::delete('', 'SkinlibController@delete')->name('delete'); + }); + }); +}); + Route::prefix('skinlib')->name('skinlib.')->group(function () { Route::view('', 'skinlib.index')->name('home'); Route::get('info/{texture}', 'SkinlibController@info')->name('info'); @@ -98,14 +111,7 @@ Route::prefix('skinlib')->name('skinlib.')->group(function () { Route::get('list', 'SkinlibController@library')->name('list'); Route::middleware(['authorize', 'verified'])->group(function () { - Route::prefix('upload')->name('upload')->group(function () { - Route::get('', 'SkinlibController@upload'); - Route::post('', 'SkinlibController@handleUpload'); - }); - Route::post('model', 'SkinlibController@model'); - Route::post('rename', 'SkinlibController@rename'); - Route::post('privacy', 'SkinlibController@privacy'); - Route::post('delete', 'SkinlibController@delete'); + Route::get('upload', 'SkinlibController@upload'); Route::post('report', 'ReportController@submit'); }); }); diff --git a/scripts/generateUrls.ts b/scripts/generateUrls.ts index 921f8e6e..1723de35 100644 --- a/scripts/generateUrls.ts +++ b/scripts/generateUrls.ts @@ -4,7 +4,7 @@ import ts from 'typescript' import prettier from 'prettier' type Route = { uri: string; name: string | null } -const supportedPrefixes = ['auth.', 'user.', 'skinlib.', 'admin.'] +const supportedPrefixes = ['auth.', 'user.', 'skinlib.', 'texture.', 'admin.'] type TreeObject = { [key: string]: Tree } type Tree = TreeObject | string @@ -71,19 +71,21 @@ function parseURI(uri: string): ts.ArrowFunction { } function parseTree(tree: Tree): ts.ObjectLiteralExpression { - const properties = Object.entries(tree).map(([key, value]) => { - if (typeof value === 'string') { - return ts.createPropertyAssignment( - ts.createIdentifier(key), - parseURI(value), - ) - } else { - return ts.createPropertyAssignment( - ts.createIdentifier(key), - parseTree(value), - ) - } - }) + const properties = Object.entries(tree) + .sort(([a], [b]) => (a > b ? 1 : -1)) + .map(([key, value]) => { + if (typeof value === 'string') { + return ts.createPropertyAssignment( + ts.createIdentifier(key), + parseURI(value), + ) + } else { + return ts.createPropertyAssignment( + ts.createIdentifier(key), + parseTree(value), + ) + } + }) return ts.createObjectLiteral(properties) } diff --git a/tests/HttpTest/ControllersTest/SkinlibControllerTest.php b/tests/HttpTest/ControllersTest/SkinlibControllerTest.php index 21f4e45b..0297ba94 100644 --- a/tests/HttpTest/ControllersTest/SkinlibControllerTest.php +++ b/tests/HttpTest/ControllersTest/SkinlibControllerTest.php @@ -17,6 +17,25 @@ class SkinlibControllerTest extends TestCase { use DatabaseTransactions; + public function testAccessControl() + { + Storage::fake('textures'); + + $other = factory(User::class)->create(); + $texture = factory(Texture::class)->create(); + + // other user should not be able to delete + $this->actingAs($other) + ->deleteJson(route('texture.delete', ['texture' => $texture])) + ->assertJson(['code' => 1]) + ->assertForbidden(); + + // administrators can delete it + $this->actingAs(factory(User::class)->states('admin')->create()) + ->deleteJson(route('texture.delete', ['texture' => $texture])) + ->assertJson(['code' => 0]); + } + public function testLibrary() { $steve = factory(Texture::class)->create([ @@ -192,7 +211,7 @@ class SkinlibControllerTest extends TestCase public function testInfo() { $texture = factory(Texture::class)->create(); - $this->get(route('skinlib.info', ['texture' => $texture])) + $this->get(route('texture.info', ['texture' => $texture])) ->assertJson($texture->toArray()); } @@ -217,7 +236,7 @@ class SkinlibControllerTest extends TestCase // without file $this->actingAs($user) - ->postJson('/skinlib/upload', [ + ->postJson(route('texture.upload'), [ 'name' => 'name', 'type' => 'steve', 'public' => true, @@ -232,7 +251,7 @@ class SkinlibControllerTest extends TestCase UPLOAD_ERR_NO_TMP_DIR, true ); - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'name', 'file' => $upload, 'type' => 'steve', @@ -240,16 +259,17 @@ class SkinlibControllerTest extends TestCase ])->assertJsonValidationErrors('file'); // without `name` field - $this->postJson('/skinlib/upload')->assertJsonValidationErrors('name'); + $this->postJson(route('texture.upload')) + ->assertJsonValidationErrors('name'); // specified regular expression for texture name option(['texture_name_regexp' => '/\\d+/']); - $this->postJson('/skinlib/upload', ['name' => 'abc']) + $this->postJson(route('texture.upload'), ['name' => 'abc']) ->assertJsonValidationErrors('name'); option(['texture_name_regexp' => null]); // not a PNG file - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'file' => UploadedFile::fake()->create('fake', 5), ])->assertJsonValidationErrors('file'); @@ -257,34 +277,34 @@ class SkinlibControllerTest extends TestCase // too large file option(['max_upload_file_size' => 2]); $upload = UploadedFile::fake()->create('large.png', 5); - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'file' => $upload, ])->assertJsonValidationErrors('file'); option(['max_upload_file_size' => 1024]); // no texture type is specified - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'file' => $file, ])->assertJsonValidationErrors('type'); // invalid texture type - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'file' => $file, 'type' => 'nope', ])->assertJsonValidationErrors('type'); // without `public` field - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'file' => $file, 'type' => 'steve', ])->assertJsonValidationErrors('public'); // invalid skin size - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'steve', @@ -297,7 +317,7 @@ class SkinlibControllerTest extends TestCase 'height' => 30, ]), ]); - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'alex', @@ -310,7 +330,7 @@ class SkinlibControllerTest extends TestCase 'height' => 50, ]), ]); - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'cape', @@ -329,7 +349,7 @@ class SkinlibControllerTest extends TestCase // score is not enough $user = factory(User::class)->create(['score' => 0]); $this->actingAs($user) - ->postJson('/skinlib/upload', [ + ->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'steve', @@ -344,7 +364,7 @@ class SkinlibControllerTest extends TestCase 'score' => (int) option('score_per_closet_item') + (int) option('score_per_storage'), ]); $this->actingAs($user)->postJson( - '/skinlib/upload', + route('texture.upload'), [ 'name' => 'texture', 'public' => false, @@ -358,7 +378,7 @@ class SkinlibControllerTest extends TestCase // success option(['score_award_per_texture' => 2]); - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'steve', @@ -419,7 +439,7 @@ class SkinlibControllerTest extends TestCase // upload a duplicated texture $user = factory(User::class)->create(); $this->actingAs($user) - ->postJson('/skinlib/upload', [ + ->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'steve', @@ -437,7 +457,7 @@ class SkinlibControllerTest extends TestCase return new Rejection('rejected'); }); - $this->postJson('/skinlib/upload', [ + $this->postJson(route('texture.upload'), [ 'name' => 'texture', 'public' => true, 'type' => 'steve', @@ -449,6 +469,7 @@ class SkinlibControllerTest extends TestCase public function testDelete() { + /** @var FilesystemAdapter */ $disk = Storage::fake('textures'); $uploader = factory(User::class)->create(); @@ -456,121 +477,97 @@ class SkinlibControllerTest extends TestCase $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); option(['return_score' => false]); - // Non-existed texture - $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => -1]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.non-existent'), - ]); - - // Other user should not be able to delete - $this->actingAs($other) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.no-permission'), - ]); - - // Administrators can delete it - $this->actingAs(factory(User::class)->states('admin')->create()) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.delete.success'), - ]); - $this->assertNull(Texture::find($texture->tid)); - - $texture = factory(Texture::class)->create(); - factory(Texture::class)->create(['hash' => $texture->hash]); + $duplicate = factory(Texture::class)->create([ + 'hash' => $texture->hash, + 'uploader' => $uploader->uid, + ]); $disk->put($texture->hash, ''); - // When file is occupied, the file should not be deleted - $this->postJson('/skinlib/delete', ['tid' => $texture->tid]) + // when file is occupied, the file should not be deleted + $this->actingAs($uploader) + ->deleteJson(route('texture.delete', ['texture' => $duplicate])) + ->assertJson([ + 'code' => 0, + 'message' => trans('skinlib.delete.success'), + ]); + $this->assertNull(Texture::find($duplicate->tid)); + $disk->assertExists($texture->hash); + + $this->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson([ 'code' => 0, 'message' => trans('skinlib.delete.success'), ]); $this->assertNull(Texture::find($texture->tid)); - $this->assertTrue(Storage::disk('textures')->exists($texture->hash)); + $disk->assertMissing($texture->hash); - $texture = factory(Texture::class)->create(); - factory(Texture::class)->create(['hash' => $texture->hash]); - $this->postJson('/skinlib/delete', ['tid' => $texture->tid]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.delete.success'), - ]); - $this->assertNull(Texture::find($texture->tid)); - $this->assertFalse(Storage::disk('textures')->exists($texture->hash)); - - // Return score + // return score option(['return_score' => true]); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) + ->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson([ 'code' => 0, 'message' => trans('skinlib.delete.success'), ]); $this->assertEquals( $uploader->score + $texture->size * option('score_per_storage'), - User::find($uploader->uid)->score + $uploader->fresh()->score ); - $uploader = User::find($uploader->uid); + $uploader->refresh(); $texture = factory(Texture::class)->create([ 'uploader' => $uploader->uid, 'public' => false, ]); $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) + ->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson([ 'code' => 0, 'message' => trans('skinlib.delete.success'), ]); $this->assertEquals( $uploader->score + $texture->size * option('private_score_per_storage'), - User::find($uploader->uid)->score + $uploader->fresh()->score ); option(['return_score' => false]); - // Return the award + // return the award option(['score_award_per_texture' => 5]); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $uploader->refresh(); $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) + ->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson(['code' => 0]); - $this->assertEquals($uploader->score - 5, User::find($uploader->uid)->score); - // Option disabled + $this->assertEquals($uploader->score - 5, $uploader->fresh()->score); + // option disabled option(['take_back_scores_after_deletion' => false]); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $uploader->refresh(); $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) + ->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson(['code' => 0]); - $this->assertEquals($uploader->score, User::find($uploader->uid)->score); - // Private texture + $this->assertEquals($uploader->score, $uploader->fresh()->score); + // private texture $texture = factory(Texture::class)->create([ 'uploader' => $uploader->uid, 'public' => false, ]); $uploader->refresh(); $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) + ->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson(['code' => 0]); - $this->assertEquals($uploader->score, User::find($uploader->uid)->score); + $this->assertEquals($uploader->score, $uploader->fresh()->score); - // Remove from closet + // remove from closet option(['return_score' => true]); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $other->closet()->attach($texture->tid, ['item_name' => 'a']); $other->score = 0; $other->save(); $this->actingAs($uploader) - ->postJson('/skinlib/delete', ['tid' => $texture->tid]) + ->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson(['code' => 0]); $other->refresh(); $this->assertEquals(option('score_per_closet_item'), $other->score); @@ -582,56 +579,30 @@ class SkinlibControllerTest extends TestCase $other = factory(User::class)->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - // Non-existed texture - $this->actingAs($uploader) - ->postJson('/skinlib/privacy', ['tid' => -1]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.non-existent'), - ]); - - // Other user should not be able to set privacy - $this->actingAs($other) - ->postJson('/skinlib/privacy', ['tid' => $texture->tid]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.no-permission'), - ]); - - // Administrators can change it - $uploader->score += $texture->size * option('private_score_per_storage'); - $uploader->save(); - $this->actingAs(factory(User::class)->states('admin')->create()) - ->postJson('/skinlib/privacy', ['tid' => $texture->tid]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), - ]); - $this->assertEquals(0, Texture::find($texture->tid)->public); - - // Setting a texture to be private needs more scores + // setting a texture to be private needs more scores $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $uploader->score = 0; $uploader->save(); $this->actingAs($uploader) - ->postJson('/skinlib/privacy', ['tid' => $texture->tid]) + ->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson([ 'code' => 1, 'message' => trans('skinlib.upload.lack-score'), ]); - $this->assertEquals(1, Texture::find($texture->tid)->public); + $this->assertTrue($texture->fresh()->public); $texture->public = true; $texture->save(); $uploader->score = $texture->size * (option('private_score_per_storage') - option('score_per_storage')); $uploader->save(); - $this->postJson('/skinlib/privacy', ['tid' => $texture->tid]) + $this->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson([ 'code' => 0, 'message' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), ]); - $this->assertEquals(0, User::find($uploader->uid)->score); + $this->assertEquals(0, $uploader->fresh()->score); + $this->assertFalse($texture->fresh()->public); // When setting a texture to be private, // other players should not be able to use it. @@ -641,19 +612,19 @@ class SkinlibControllerTest extends TestCase $player = factory(Player::class)->create(['tid_skin' => $texture->tid]); $other = factory(User::class)->create(); $other->closet()->attach($texture->tid, ['item_name' => 'a']); - $this->postJson('/skinlib/privacy', ['tid' => $texture->tid]) + $this->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson([ 'code' => 0, 'message' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), ]); - $this->assertEquals(0, Player::find($player->pid)->tid_skin); + $this->assertEquals(0, $player->fresh()->tid_skin); $this->assertEquals(0, $other->closet()->count()); $this->assertEquals( $other->score + option('score_per_closet_item'), - User::find($other->uid)->score + $other->fresh()->score ); - // Take back the score + // take back the score option(['score_award_per_texture' => 5]); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $uploader->score = $texture->size * ( @@ -661,159 +632,92 @@ class SkinlibControllerTest extends TestCase ); $uploader->score += option('score_award_per_texture'); $uploader->save(); - $this->postJson('/skinlib/privacy', ['tid' => $texture->tid]) + $this->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson(['code' => 0]); - $this->assertEquals(0, User::find($uploader->uid)->score); + $this->assertEquals(0, $uploader->fresh()->score); - // Without returning score + // without returning score option(['return_score' => false, 'private_score_per_storage' => 0]); $uploader->score += 1000; $uploader->save(); $texture = factory(Texture::class)->create(['public' => 'false', 'uploader' => $uploader->uid]); $other = factory(User::class)->create(); $other->closet()->attach($texture->tid, ['item_name' => 'a']); - $this->postJson('/skinlib/privacy', ['tid' => $texture->tid]) + $this->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson(['code' => 0]); - $this->assertEquals($other->score, User::find($other->uid)->score); + $this->assertEquals($other->score, $other->fresh()->score); } public function testRename() { $uploader = factory(User::class)->create(); - $other = factory(User::class)->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - // Without `tid` field + // without `name` field $this->actingAs($uploader) - ->postJson('/skinlib/rename') - ->assertJsonValidationErrors('tid'); + ->putJson(route('texture.name', ['texture' => $texture])) + ->assertJsonValidationErrors('name'); - // `tid` is not a integer - $this->postJson('/skinlib/rename', [ - 'tid' => 'str', - ]) - ->assertJsonValidationErrors('tid'); + // specified regular expression for texture name + option(['texture_name_regexp' => '/\\d+/']); + $this->putJson( + route('texture.name', ['texture' => $texture]), + ['name' => 'abc'] + )->assertJsonValidationErrors('name'); + option(['texture_name_regexp' => null]); - // Without `new_name` field - $this->postJson('/skinlib/rename', [ - 'tid' => $texture->tid, - ]) - ->assertJsonValidationErrors('new_name'); - - // Non-existed texture - $this->postJson('/skinlib/rename', [ - 'tid' => -1, - 'new_name' => 'name', - ]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.non-existent'), - ]); - - // Other user should not be able to rename - $this->actingAs($other) - ->postJson('/skinlib/rename', [ - 'tid' => $texture->tid, - 'new_name' => 'name', - ]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.no-permission'), - ]); - - // Administrators should be able to rename - $this->actingAs(factory(User::class)->states('admin')->create()) - ->postJson('/skinlib/rename', [ - 'tid' => $texture->tid, - 'new_name' => 'name', - ]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.rename.success', ['name' => 'name']), - ]); - $this->assertEquals('name', Texture::find($texture->tid)->name); - - // Uploader should be able to rename - $this->actingAs($uploader) - ->postJson('/skinlib/rename', [ - 'tid' => $texture->tid, - 'new_name' => 'new_name', - ]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.rename.success', ['name' => 'new_name']), - ]); - $this->assertEquals('new_name', Texture::find($texture->tid)->name); + // success + $this->putJson( + route('texture.name', ['texture' => $texture]), + ['name' => 'abc'] + )->assertJson([ + 'code' => 0, + 'message' => trans('skinlib.rename.success', ['name' => 'abc']), + ]); + $this->assertEquals('abc', $texture->fresh()->name); } - public function testModel() + public function testType() { $uploader = factory(User::class)->create(); $other = factory(User::class)->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - // Non-existed texture + // missing `type` field $this->actingAs($uploader) - ->postJson('/skinlib/model', [ - 'tid' => -1, - 'model' => 'alex', - ]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.non-existent'), - ]); + ->putJson(route('texture.type', ['texture' => $texture])) + ->assertJsonValidationErrors('type'); - // Other user should not be able to change model - $this->actingAs($other) - ->postJson('/skinlib/model', [ - 'tid' => $texture->tid, - 'model' => 'alex', - ]) - ->assertJson([ - 'code' => 1, - 'message' => trans('skinlib.no-permission'), - ]); + // invalid type + $this->putJson( + route('texture.type', ['texture' => $texture]), + ['type' => 'nope'] + )->assertJsonValidationErrors('type'); - // Administrators should be able to change model - $this->actingAs(factory(User::class)->states('admin')->create()) - ->postJson('/skinlib/model', [ - 'tid' => $texture->tid, - 'model' => 'alex', - ]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.model.success', ['model' => 'alex']), - ]); - $this->assertEquals('alex', Texture::find($texture->tid)->type); - - // Uploader should be able to change model - $this->actingAs($uploader) - ->postJson('/skinlib/model', [ - 'tid' => $texture->tid, - 'model' => 'steve', - ]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.model.success', ['model' => 'steve']), - ]); - $this->assertEquals('steve', Texture::find($texture->tid)->type); + // success + $this->putJson( + route('texture.type', ['texture' => $texture]), + ['type' => 'steve'] + )->assertJson([ + 'code' => 0, + 'message' => trans('skinlib.model.success', ['model' => 'steve']), + ]); + $this->assertEquals('steve', $texture->fresh()->type); $duplicate = factory(Texture::class)->states('alex')->create([ 'uploader' => $other->uid, 'hash' => $texture->hash, ]); - // Allow private texture + // allow private texture $duplicate->public = false; $duplicate->save(); - $this->actingAs($uploader) - ->postJson('/skinlib/model', [ - 'tid' => $texture->tid, - 'model' => 'alex', - ]) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.model.success', ['model' => 'alex']), - ]); + $this->putJson( + route('texture.type', ['texture' => $texture]), + ['type' => 'alex'] + )->assertJson([ + 'code' => 0, + 'message' => trans('skinlib.model.success', ['model' => 'alex']), + ]); } }