refactor SkinlibController

This commit is contained in:
Pig Fang 2020-06-05 23:35:49 +08:00
parent 160dbff834
commit 946896117a
9 changed files with 278 additions and 401 deletions

View File

@ -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);
}
}

View File

@ -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 },

View File

@ -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<Texture>(url)
setTexture(texture)
@ -68,12 +69,9 @@ const Show: React.FC = () => {
return
}
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/skinlib/rename',
{
tid: texture.tid,
new_name: name,
},
const { code, message } = await fetch.put<fetch.ResponseBody>(
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<fetch.ResponseBody>(
'/skinlib/model',
{
tid: texture.tid,
model: type,
},
const { code, message } = await fetch.put<fetch.ResponseBody>(
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<fetch.ResponseBody>(
'/skinlib/privacy',
{ tid: texture.tid },
const { code, message } = await fetch.put<fetch.ResponseBody>(
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<fetch.ResponseBody>(
'/skinlib/delete',
{ tid: texture.tid },
const { code, message } = await fetch.del<fetch.ResponseBody>(
urls.texture.delete(texture.tid),
)
if (code === 0) {
toast.success(message)

View File

@ -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) {

View File

@ -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(<Show />)
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(<Show />)
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(<Show />)
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(<Show />)
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(<Show />)
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')

View File

@ -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),
)

View File

@ -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');
});
});

View File

@ -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)
}

View File

@ -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']),
]);
}
}