diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php index 94c8a429..8e0406e7 100644 --- a/app/Http/Controllers/SkinlibController.php +++ b/app/Http/Controllers/SkinlibController.php @@ -38,7 +38,7 @@ class SkinlibController extends Controller * Available Query String: filter, uploader, page, sort, keyword, items_per_page. * * @param Request $request [description] - * @return [type] [description] + * @return JsonResponse */ public function getSkinlibFiltered(Request $request) { @@ -59,6 +59,7 @@ class SkinlibController extends Controller // How many items to show in one page $itemsPerPage = $request->input('items_per_page', 20); + $itemsPerPage = $itemsPerPage <= 0 ? 20 : $itemsPerPage; // Keyword to search $keyword = $request->input('keyword', ''); @@ -95,7 +96,7 @@ class SkinlibController extends Controller } } - $totalPages = ceil($query->count() / 20); + $totalPages = ceil($query->count() / $itemsPerPage); $textures = $query->orderBy($sortBy, 'desc') ->skip(($currentPage - 1) * $itemsPerPage) @@ -161,8 +162,8 @@ class SkinlibController extends Controller $t->name = $request->input('name'); $t->type = $request->input('type'); $t->likes = 1; - $t->hash = Utils::upload($_FILES['file']); - $t->size = ceil($_FILES['file']['size'] / 1024); + $t->hash = Utils::upload($request->file('file')); + $t->size = ceil($request->file('file')->getSize() / 1024); $t->public = ($request->input('public') == 'true') ? "1" : "0"; $t->uploader = $this->user->uid; $t->upload_at = Utils::getTimeFormatted(); @@ -177,7 +178,7 @@ class SkinlibController extends Controller if (!$results->isEmpty()) { foreach ($results as $result) { - // if the texture already uploaded was setted to private, + // if the texture already uploaded was set to private, // then allow to re-upload it. if ($result->type == $t->type && $result->public == "1") { return json(trans('skinlib.upload.repeated'), 0, [ @@ -196,7 +197,7 @@ class SkinlibController extends Controller 'tid' => $t->tid ]); } - } + } // @codeCoverageIgnore public function delete(Request $request, UserRepository $users) { @@ -211,8 +212,8 @@ class SkinlibController extends Controller } // check if file occupied - if (Texture::where('hash', $result['hash'])->count() == 1) { - Storage::delete($result['hash']); + if (Texture::where('hash', $result->hash)->count() == 1) { + Storage::disk('textures')->delete($result->hash); } if (option('return_score')) { @@ -232,13 +233,11 @@ class SkinlibController extends Controller if ($result->delete()) { return json(trans('skinlib.delete.success'), 0); } - } + } // @codeCoverageIgnore public function privacy(Request $request, UserRepository $users) { $t = Texture::find($request->input('tid')); - $type = $t->type; - $uid = session('uid'); if (!$t) return json(trans('skinlib.non-existent'), 1); @@ -251,9 +250,13 @@ class SkinlibController extends Controller return json(trans('skinlib.upload.lack-score'), 1); } - foreach (Player::where("tid_$type", $t->tid)->where('uid', '<>', $uid)->get() as $player) { - $player->setTexture(["tid_$type" => 0]); - } + $type = $t->type; + Player::where("tid_$type", $t->tid) + ->where('uid', '<>', session('uid')) + ->get() + ->each(function ($player) use ($type) { + $player->setTexture(["tid_$type" => 0]); + }); @$users->get($t->uploader)->setScore($score_diff, 'plus'); @@ -264,7 +267,7 @@ class SkinlibController extends Controller 'public' => $t->public ]); } - } + } // @codeCoverageIgnore public function rename(Request $request) { $this->validate($request, [ @@ -285,13 +288,13 @@ class SkinlibController extends Controller if ($t->save()) { return json(trans('skinlib.rename.success', ['name' => $request->input('new_name')]), 0); } - } + } // @codeCoverageIgnore /** * Check Uploaded Files * * @param Request $request - * @return void + * @return JsonResponse */ private function checkUpload(Request $request) { @@ -307,16 +310,13 @@ class SkinlibController extends Controller 'public' => 'required' ]); - if ($_FILES['file']['type'] != "image/png" && $_FILES['file']['type'] != "image/x-png") { + $mime = $request->file('file')->getMimeType(); + if ($mime != "image/png" && $mime != "image/x-png") { return json(trans('skinlib.upload.type-error'), 1); } - // if error occured while uploading file - if ($_FILES['file']["error"] > 0) - return json($_FILES['file']["error"], 1); - $type = $request->input('type'); - $size = getimagesize($_FILES['file']["tmp_name"]); + $size = getimagesize($request->file('file')); $ratio = $size[0] / $size[1]; if ($type == "steve" || $type == "alex") { @@ -330,6 +330,6 @@ class SkinlibController extends Controller } else { return json(trans('general.illegal-parameters'), 1); } - } + } // @codeCoverageIgnore } diff --git a/app/Services/Utils.php b/app/Services/Utils.php index 07813cf2..1b556ee7 100644 --- a/app/Services/Utils.php +++ b/app/Services/Utils.php @@ -120,36 +120,23 @@ class Utils /** * Rename uploaded file * - * @param array $file files uploaded via HTTP POST + * @param \Illuminate\Http\UploadedFile $file files uploaded via HTTP POST * @return string $hash sha256 hash of file + * @throws \Exception */ public static function upload($file) { - $path = 'tmp'.time(); - $absolute_path = storage_path("textures/$path"); - + $hash = hash_file('sha256', $file); try { - if (false === move_uploaded_file($file['tmp_name'], $absolute_path)) { - throw new \Exception('Failed to remove uploaded files, please check the permission', 1); + $storage = Storage::disk('textures'); + if (!$storage->exists($hash)) { + $storage->put($hash, file_get_contents($file)); } } catch (\Exception $e) { - Log::warning("Failed to move uploaded file, $e"); - } finally { - if (file_exists($absolute_path)) { - $hash = hash_file('sha256', $absolute_path); - - if (! Storage::disk('textures')->has($hash)) { - Storage::disk('textures')->move($path, $hash); - } else { - // delete the temp file - unlink($absolute_path); - } - - return $hash; - } else { - Log::warning("Failed to upload file $path"); - } + Log::warning("Failed to upload file {$file->getFilename()}"); + throw new \Exception("Failed to upload file {$file->getFilename()}"); } + return $hash; } public static function download($url, $path) diff --git a/tests/SkinlibControllerTest.php b/tests/SkinlibControllerTest.php new file mode 100644 index 00000000..35567c0d --- /dev/null +++ b/tests/SkinlibControllerTest.php @@ -0,0 +1,936 @@ +vfs_root = vfsStream::setup(); + } + + protected function serializeTextures($textures) { + return $textures + ->map(function ($texture) { + return [ + 'tid' => $texture->tid, + 'name' => $texture->name, + 'type' => $texture->type, + 'likes' => $texture->likes, + 'hash' => $texture->hash, + 'size' => $texture->size, + 'uploader' => $texture->uploader, + 'public' => $texture->public ? 1 : 0, + 'upload_at' => $texture->upload_at->format('Y-m-d H:i:s') + ]; + }) + ->all(); + } + + public function testIndex() + { + $this->visit('/skinlib') + ->seePageIs('/skinlib') + ->assertViewHas('user'); + } + + public function testGetSkinlibFiltered() + { + $this->get('/skinlib/data') + ->seeJson([ + 'items' => [], + 'anonymous' => true, + 'total_pages' => 0 + ]); + + $steves = factory(Texture::class)->times(5)->create(); + $alexs = factory(Texture::class, 'alex')->times(5)->create(); + $skins = $steves->merge($alexs); + $capes = factory(Texture::class, 'cape')->times(5)->create(); + + // Default arguments + $expected = $skins + ->sortByDesc('upload_at') + ->values(); // WTF! DO NOT FORGET IT!! + $this->get('/skinlib/data') + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // Only steve + $expected = $steves + ->sortByDesc('upload_at') + ->values(); + $this->get('/skinlib/data?filter=steve') + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // Invalid type + $this->get('/skinlib/data?filter=what') + ->seeJson([ + 'items' => [], + 'anonymous' => true, + 'total_pages' => 0 + ]); + + // Only capes + $expected = $capes + ->sortByDesc('upload_at') + ->values(); + $this->get('/skinlib/data?filter=cape') + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // Only specified uploader + $uid = $skins->random()->uploader; + $expected = $skins + ->filter(function ($texture) use ($uid) { + return $texture->uploader == $uid; + }) + ->sortByDesc('upload_at') + ->values(); + $this->get('/skinlib/data?uploader='.$uid) + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // Sort by `likes` + $expected = $skins + ->sortByDesc('likes') + ->values(); + $this->get('/skinlib/data?sort=likes') + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // Search + $keyword = str_limit($skins->random()->name, 1, ''); + $expected = $skins + ->filter(function ($texture) use ($keyword) { + return str_contains($texture->name, $keyword) || + str_contains($texture->name, strtolower($keyword)); + }) + ->sortByDesc('upload_at') + ->values(); + $this->get('/skinlib/data?keyword='.$keyword) + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // More than one argument + $keyword = str_limit($skins->random()->name, 1, ''); + $expected = $skins + ->filter(function ($texture) use ($keyword) { + return str_contains($texture->name, $keyword) || + str_contains($texture->name, strtolower($keyword)); + }) + ->sortByDesc('likes') + ->values(); + $this->get('/skinlib/data?sort=likes&keyword='.$keyword) + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 1 + ]); + + // Pagination + $steves = factory(Texture::class) + ->times(15) + ->create() + ->merge($steves); + $skins = $steves->merge($alexs); + $expected = $skins + ->sortByDesc('upload_at') + ->values() + ->forPage(1, 20); + $expected = $this->serializeTextures($expected); + $this->get('/skinlib/data') + ->seeJson([ + 'items' => $expected, + 'anonymous' => true, + 'total_pages' => 2 + ]); + $this->get('/skinlib/data?page=-5') + ->seeJson([ + 'items' => $expected, + 'anonymous' => true, + 'total_pages' => 2 + ]); + $expected = $skins + ->sortByDesc('upload_at') + ->values() + ->forPage(2, 20) + ->values(); + $expected = $this->serializeTextures($expected); + $this->get('/skinlib/data?page=2') + ->seeJson([ + 'items' => $expected, + 'anonymous' => true, + 'total_pages' => 2 + ]); + $this->get('/skinlib/data?page=8') + ->seeJson([ + 'items' => [], + 'anonymous' => true, + 'total_pages' => 2 + ]); + $this->get('/skinlib/data?items_per_page=-6&page=2') + ->seeJson([ + 'items' => $expected, + 'anonymous' => true, + 'total_pages' => 2 + ]); + $expected = $skins + ->sortByDesc('upload_at') + ->values() + ->forPage(3, 8) + ->values(); + $this->get('/skinlib/data?page=3&items_per_page=8') + ->seeJson([ + 'items' => $this->serializeTextures($expected), + 'anonymous' => true, + 'total_pages' => 4 + ]); + + // Add some private textures + $uploader = factory(User::class)->create(); + $otherUser = factory(User::class)->create(); + $private = factory(Texture::class) + ->times(5) + ->create(['public' => false, 'uploader' => $uploader->uid]); + + // If not logged in, private textures should not be shown + $expected = $skins + ->sortByDesc('upload_at') + ->values() + ->forPage(1, 20); + $expected = $this->serializeTextures($expected); + $this->get('/skinlib/data') + ->seeJson([ + 'items' => $expected, + 'anonymous' => true, + 'total_pages' => 2 + ]); + + // Other users should not see someone's private textures + for ($i = 0; $i < count($expected); $i++) { + $expected[$i]['liked'] = false; + } + $this->actAs($otherUser) + ->get('/skinlib/data') + ->seeJson([ + 'items' => $expected, + 'anonymous' => false, + 'total_pages' => 2 + ]); + + // A user has added a texture from skin library to his closet + $texture = $skins + ->sortByDesc('upload_at') + ->values() + ->first(); + $closet = new Closet($otherUser->uid); + $closet->add($texture->tid, $texture->name); + $closet->save(); + for ($i = 0; $i < count($expected); $i++) { + if ($expected[$i]['tid'] == $texture->tid) { + $expected[$i]['liked'] = true; + } else { + $expected[$i]['liked'] = false; + } + } + $this->get('/skinlib/data') + ->seeJson([ + 'items' => $expected, + 'anonymous' => false, + 'total_pages' => 2 + ]); + + // Uploader can see his private textures + $expected = $skins + ->merge($private) + ->sortByDesc('upload_at') + ->values() + ->forPage(1, 20); + $expected = $this->serializeTextures($expected); + for ($i = 0; $i < count($expected); $i++) { + // The reason we use `false` here is that some textures just were + // uploaded by this user, but these textures are not in his closet. + // By default(not in testing like now), when you uploaded a texture, + // that texture will be added to your closet. + // So here, we can assume that a user upload some textures, but he + // has deleted them from his closet. + $expected[$i]['liked'] = false; + } + $this->actAs($uploader) + ->get('/skinlib/data') + ->seeJson([ + 'items' => $expected, + 'anonymous' => false, + 'total_pages' => 2 + ]); + + // Administrators can see private textures + $admin = factory(User::class, 'admin')->create(); + $this->actAs($admin) + ->get('/skinlib/data') + ->seeJson([ + 'items' => $expected, + 'anonymous' => false, + 'total_pages' => 2 + ]); + } + + public function testShow() + { + // Cannot find texture + $this->get('/skinlib/show/1') + ->see(trans('skinlib.show.deleted')); + + // Invalid texture + option(['auto_del_invalid_texture' => false]); + $texture = factory(Texture::class)->create(); + $this->get('/skinlib/show/'.$texture->tid) + ->see(trans('skinlib.show.deleted').trans('skinlib.show.contact-admin')); + $this->assertNotNull(Texture::find($texture->tid)); + + option(['auto_del_invalid_texture' => true]); + $this->get('/skinlib/show/'.$texture->tid) + ->see(trans('skinlib.show.deleted')); + $this->assertNull(Texture::find($texture->tid)); + + // Show a texture + $texture = factory(Texture::class)->create(); + Storage::disk('textures')->put($texture->hash, ''); + $this->get('/skinlib/show/'.$texture->tid) + ->assertViewHas('texture') + ->assertViewHas('with_out_filter', true) + ->assertViewHas('user'); + + // Guest should not see private texture + $uploader = factory(User::class)->create(); + $texture = factory(Texture::class)->create([ + 'uploader' => $uploader->uid, + 'public' => false + ]); + Storage::disk('textures')->put($texture->hash, ''); + $this->get('/skinlib/show/'.$texture->tid) + ->see(trans('skinlib.show.private')); + + // Other user should not see private texture + $this->actAs('normal') + ->get('/skinlib/show/'.$texture->tid) + ->see(trans('skinlib.show.private')); + + // Administrators should be able to see private textures + $this->actAs('admin') + ->get('/skinlib/show/'.$texture->tid) + ->assertViewHas('texture'); + + // Uploader should be able to see private textures + $this->actAs($uploader) + ->get('/skinlib/show/'.$texture->tid) + ->assertViewHas('texture'); + } + + public function testInfo() + { + // Non-existed texture + $this->get('/skinlib/info/1') + ->seeJson([]); + + $texture = factory(Texture::class)->create(); + $this->get('/skinlib/info/'.$texture->tid) + ->seeJson([ + 'tid' => $texture->tid, + 'name' => $texture->name, + 'type' => $texture->type, + 'likes' => $texture->likes, + 'hash' => $texture->hash, + 'size' => $texture->size, + 'uploader' => $texture->uploader, + 'public' => $texture->public ? 1 : 0, + 'upload_at' => $texture->upload_at->format('Y-m-d H:i:s') + ]); + } + + public function testUpload() + { + $this->actAs('normal') + ->visit('/skinlib/upload') + ->seePageIs('/skinlib/upload') + ->assertViewHas('user') + ->assertViewHas('with_out_filter', true); + } + + public function testHandleUpload() + { + // Some error occurred when uploading file + $file = vfsStream::newFile('test.png') + ->at($this->vfs_root); + $upload = new UploadedFile( + $file->url(), + $file->getName(), + 'image/png', + 50, + UPLOAD_ERR_NO_TMP_DIR, + true + ); + $this->actAs('normal') + ->call( + 'POST', + '/skinlib/upload', + [], + [], + ['file' => $upload] + ) + ->getContent(); + $this->seeJson([ + 'errno' => UPLOAD_ERR_NO_TMP_DIR, + 'msg' => Utils::convertUploadFileError(UPLOAD_ERR_NO_TMP_DIR) + ]); + + // Without `name` field + $this->post('/skinlib/upload', [], [ + 'X-Requested-With' => 'XMLHttpRequest' + ])->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.required', ['attribute' => 'Name']) + ]); + + // With some special chars + $this->post('/skinlib/upload', [ + 'name' => '\\' + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ])->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.no_special_chars', ['attribute' => 'Name']) + ]); + + // Without file + $this->post('/skinlib/upload', [ + 'name' => 'texture' + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ])->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.required', ['attribute' => 'File']) + ]); + + // Too large file + option(['max_upload_file_size' => 2]); + $this->post('/skinlib/upload', [ + 'name' => 'texture', + 'file' => $upload + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ])->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.max.file', ['attribute' => 'File', 'max' => '2']) + ]); + option(['max_upload_file_size' => 1024]); + + // Without `public` field + $this->post('/skinlib/upload', [ + 'name' => 'texture', + 'file' => 'content' // Though it is not a file, it is OK + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ])->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.required', ['attribute' => 'public']) + ]); + + // Not a PNG image + $upload = new UploadedFile( + $file->url(), + $file->getName(), + 'image/jpeg', + 2, + null, + true + ); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.upload.type-error') + ]); + + // No texture type is specified + $file = vfsStream::newFile('test.png') + ->at($this->vfs_root); + imagepng(imagecreatetruecolor(64, 32), $file->url()); + $upload = new UploadedFile($file->url(), $file->getName(), 'image/x-png', 2, null, true); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 1, + 'msg' => trans('general.illegal-parameters') + ]); + + // Invalid skin size + imagepng(imagecreatetruecolor(64, 30), $file->url()); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true', + 'type' => 'steve' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 1, + 'msg' => trans( + 'skinlib.upload.invalid-size', + [ + 'type' => trans('general.skin'), + 'width' => 64, + 'height' => 30 + ] + ) + ]); + imagepng(imagecreatetruecolor(100, 50), $file->url()); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true', + 'type' => 'alex' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 1, + 'msg' => trans( + 'skinlib.upload.invalid-hd-skin', + [ + 'type' => trans('general.skin'), + 'width' => 100, + 'height' => 50 + ] + ) + ]); + imagepng(imagecreatetruecolor(64, 30), $file->url()); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true', + 'type' => 'cape' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 1, + 'msg' => trans( + 'skinlib.upload.invalid-size', + [ + 'type' => trans('general.cape'), + 'width' => 64, + 'height' => 30 + ] + ) + ]); + + imagepng(imagecreatetruecolor(64, 32), $file->url()); + $upload = new UploadedFile($file->url(), $file->getName(), 'image/png', 1, null, true); + + // Score is not enough + $user = factory(User::class)->create(['score' => 0]); + $this->actAs($user) + ->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true', + 'type' => 'steve' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 7, + 'msg' => trans('skinlib.upload.lack-score') + ]); + + $user->score = + (int) option('score_per_closet_item') + + (int) option('score_per_storage'); + $user->save(); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'false', // Private texture cost more scores + 'type' => 'steve' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 7, + 'msg' => trans('skinlib.upload.lack-score') + ]); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true', // Public texture + 'type' => 'steve' + ], + [], + ['file' => $upload] + ); + $uploaded = Texture::where('name', 'texture')->first(); + $this->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.upload.success', ['name' => 'texture']), + 'tid' => $uploaded->tid + ]); + $this->assertEquals(0, User::find($user->uid)->score); + $this->assertTrue(Storage::disk('textures')->exists($uploaded->hash)); + $this->assertEquals('texture', $user->getCloset()->get($uploaded->tid)['name']); + $this->assertEquals('texture', $uploaded->name); + $this->assertEquals('steve', $uploaded->type); + $this->assertEquals(1, $uploaded->likes); + $this->assertEquals(1, $uploaded->size); + $this->assertEquals('1', $uploaded->public); + $this->assertEquals($user->uid, $uploaded->uploader); + + // Upload a duplicated texture + $user->score = 1000; + $user->save(); + $this->call( + 'POST', + '/skinlib/upload', + [ + 'name' => 'texture', + 'public' => 'true', + 'type' => 'steve' + ], + [], + ['file' => $upload] + ); + $this->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.upload.repeated'), + 'tid' => $uploaded->tid + ]); + } + + public function testDelete() + { + $uploader = factory(User::class)->create(); + $other = factory(User::class)->create(); + $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + option(['return_score' => false]); + + // Non-existed texture + $this->actAs($uploader) + ->post('/skinlib/delete', ['tid' => -1]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.non-existent') + ]); + + // Other user should not be able to delete + $this->actAs($other) + ->post('/skinlib/delete', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.no-permission') + ]); + + // Administrators can delete it + $this->actAs('admin') + ->post('/skinlib/delete', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.delete.success') + ]); + $this->assertNull(Texture::find($texture->tid)); + + $texture = factory(Texture::class)->create(); + factory(Texture::class)->create(['hash' => $texture->hash]); + Storage::disk('textures')->put($texture->hash, ''); + + // When file is occupied, the file should not be deleted + $this->post('/skinlib/delete', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.delete.success') + ]); + $this->assertNull(Texture::find($texture->tid)); + $this->assertTrue(Storage::disk('textures')->exists($texture->hash)); + + $texture = factory(Texture::class)->create(); + factory(Texture::class)->create(['hash' => $texture->hash]); + $this->post('/skinlib/delete', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.delete.success') + ]); + $this->assertNull(Texture::find($texture->tid)); + $this->assertFalse(Storage::disk('textures')->exists($texture->hash)); + + // Return score + option(['return_score' => true]); + $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + $this->actAs($uploader) + ->post('/skinlib/delete', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.delete.success') + ]); + $this->assertEquals( + $uploader->score + $texture->size * option('score_per_storage'), + User::find($uploader->uid)->score + ); + + $uploader = User::find($uploader->uid); + $texture = factory(Texture::class)->create([ + 'uploader' => $uploader->uid, + 'public' => false + ]); + $this->actAs($uploader) + ->post('/skinlib/delete', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.delete.success') + ]); + $this->assertEquals( + $uploader->score + $texture->size * option('private_score_per_storage'), + User::find($uploader->uid)->score + ); + } + + public function testPrivacy() + { + $uploader = factory(User::class)->create(); + $other = factory(User::class)->create(); + $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + + // Non-existed texture + $this->actAs($uploader) + ->post('/skinlib/privacy', ['tid' => -1]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.non-existent') + ]); + + // Other user should not be able to set privacy + $this->actAs($other) + ->post('/skinlib/privacy', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.no-permission') + ]); + + // Administrators can change it + $uploader->score += $texture->size * option('private_score_per_storage'); + $uploader->save(); + $this->actAs('admin') + ->post('/skinlib/privacy', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), + 'public' => '0' + ]); + $this->assertEquals(0, Texture::find($texture->tid)->public); + + // Setting a texture to be private needs more scores + $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + $uploader->score = 0; + $uploader->save(); + $this->actAs($uploader) + ->post('/skinlib/privacy', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.upload.lack-score') + ]); + $this->assertEquals(1, Texture::find($texture->tid)->public); + + $texture->public = true; + $texture->save(); + $uploader->score = $texture->size * + (option('private_score_per_storage') - option('score_per_storage')); + $uploader->save(); + $this->post('/skinlib/privacy', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), + 'public' => '0' + ]); + $this->assertEquals(0, User::find($uploader->uid)->score); + + // When setting a texture to be private, + // other players should not be able to use it. + $texture->public = '1'; + $texture->save(); + $uploader->score += $texture->size * option('private_score_per_storage'); + $uploader->save(); + $player = factory(Player::class)->create(['tid_steve' => $texture->tid]); + $this->post('/skinlib/privacy', ['tid' => $texture->tid]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), + 'public' => '0' + ]); + $this->assertEquals(0, Player::find($player->pid)->tid_steve); + } + + public function testRename() + { + $uploader = factory(User::class)->create(); + $other = factory(User::class)->create(); + $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + + // Without `tid` field + $this->actAs($uploader) + ->post('/skinlib/rename', [], [ + 'X-Requested-With' => 'XMLHttpRequest' + ]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.required', ['attribute' => 'tid']) + ]); + + // `tid` is not a integer + $this->post('/skinlib/rename', [ + 'tid' => 'str' + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.integer', ['attribute' => 'tid']) + ]); + + // Without `new_name` field + $this->post('/skinlib/rename', [ + 'tid' => $texture->tid + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.required', ['attribute' => 'new name']) + ]); + + // `new_name` has special chars + $this->post('/skinlib/rename', [ + 'tid' => $texture->tid, + 'new_name' => '\\' + ], [ + 'X-Requested-With' => 'XMLHttpRequest' + ]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('validation.no_special_chars', ['attribute' => 'new name']) + ]); + + // Non-existed texture + $this->post('/skinlib/rename', [ + 'tid' => -1, + 'new_name' => 'name' + ]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.non-existent') + ]); + + // Other user should not be able to rename + $this->actAs($other) + ->post('/skinlib/rename', [ + 'tid' => $texture->tid, + 'new_name' => 'name' + ]) + ->seeJson([ + 'errno' => 1, + 'msg' => trans('skinlib.no-permission') + ]); + + // Administrators should be able to rename + $this->actAs('admin') + ->post('/skinlib/rename', [ + 'tid' => $texture->tid, + 'new_name' => 'name' + ]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.rename.success', ['name' => 'name']) + ]); + $this->assertEquals('name', Texture::find($texture->tid)->name); + + // Uploader should be able to rename + $this->actAs($uploader) + ->post('/skinlib/rename', [ + 'tid' => $texture->tid, + 'new_name' => 'new_name' + ]) + ->seeJson([ + 'errno' => 0, + 'msg' => trans('skinlib.rename.success', ['name' => 'new_name']) + ]); + $this->assertEquals('new_name', Texture::find($texture->tid)->name); + } +}