Reimplementing closet

This commit is contained in:
Pig Fang 2019-03-14 23:55:49 +08:00
parent fb89859dd2
commit 5915b3ec17
20 changed files with 373 additions and 634 deletions

View File

@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use DB;
use Schema;
use App\Models\User;
use Illuminate\Console\Command;
class MigrateCloset extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bs:migrate-v4:closet';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate the closet for v4';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!Schema::hasTable('closets')) {
$this->info('Nothing to do.');
return;
}
$this->info('We will migrate all closets data. Please wait...');
$rows = DB::table('closets')->select('*')->get();
$bar = $this->output->createProgressBar($rows->count());
$rows->map(function ($row) use ($bar) {
$closet = User::find($row->uid)->closet();
collect(json_decode($row->textures, true))->each(function ($item) use ($closet) {
$closet->attach($item['tid'], ['item_name' => $item['name']]);
});
$bar->advance();
});
Schema::drop('closets');
$bar->finish();
$this->info("\nCongrats! Everything are done.");
}
}

View File

@ -17,6 +17,7 @@ class Kernel extends ConsoleKernel
Commands\KeyRandomCommand::class,
Commands\SaltRandomCommand::class,
Commands\MigratePlayersTable::class,
Commands\MigrateCloset::class,
];
/**

View File

@ -1,15 +0,0 @@
<?php
namespace App\Events;
use App\Models\Closet;
class ClosetWasFiltered extends Event
{
public $closet;
public function __construct(Closet $closet)
{
$this->closet = $closet;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Events;
use App\Models\Closet;
class ClosetWillBeFiltered extends Event
{
public $closet;
public function __construct(Closet $closet)
{
$this->closet = $closet;
}
}

View File

@ -5,29 +5,12 @@ namespace App\Http\Controllers;
use View;
use Option;
use App\Models\User;
use App\Models\Closet;
use App\Models\Texture;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ClosetController extends Controller
{
/**
* Instance of Closet.
*
* @var \App\Models\Closet
*/
private $closet;
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->closet = new Closet(Auth::id());
return $next($request);
});
}
public function index()
{
return view('user.closet')->with('user', Auth::user());
@ -37,29 +20,39 @@ class ClosetController extends Controller
{
$category = $request->input('category', 'skin');
$page = abs($request->input('page', 1));
$per_page = (int) $request->input('perPage', 6);
$perPage = (int) $request->input('perPage', 6);
$q = $request->input('q', null);
$per_page = $per_page > 0 ? $per_page : 6;
$perPage = $perPage > 0 ? $perPage : 6;
$items = collect();
$user = auth()->user();
$closet = $user->closet();
if ($q) {
// Do search
$items = $this->closet->getItems($category)->filter(function ($item) use ($q) {
return stristr($item['name'], $q);
});
if ($category == 'cape') {
$closet = $closet->where('type', 'cape');
} else {
$items = $this->closet->getItems($category);
$closet = $closet->where(function ($query) {
return $query->where('type', 'steve')->orWhere('type', 'alex');
});
}
if ($q) {
$closet = $closet->where('item_name', 'like', "%$q%");
}
$closet->offset(($page - 1) * $perPage)->limit($perPage);
// Pagination
$total_pages = ceil($items->count() / $per_page);
$items = $closet->get()->map(function ($t) {
$t->name = $t->pivot->item_name;
return $t;
});
$totalPages = ceil($items->count() / $perPage);
return response()->json([
'category' => $category,
'items' => $items->forPage($page, $per_page)->values(),
'total_pages' => $total_pages,
'items' => $items,
'total_pages' => $totalPages,
]);
}
@ -70,9 +63,9 @@ class ClosetController extends Controller
'name' => 'required|no_special_chars',
]);
$currentUser = Auth::user();
$user = Auth::user();
if ($currentUser->getScore() < option('score_per_closet_item')) {
if ($user->getScore() < option('score_per_closet_item')) {
return json(trans('user.closet.add.lack-score'), 7);
}
@ -81,19 +74,14 @@ class ClosetController extends Controller
return json(trans('user.closet.add.not-found'), 1);
}
if ($this->closet->add($tid, $request->name)) {
$t = Texture::find($tid);
$t->likes += 1;
$t->save();
$this->closet->save();
$currentUser->setScore(option('score_per_closet_item'), 'minus');
return json(trans('user.closet.add.success', ['name' => $request->input('name')]), 0);
} else {
if ($user->closet()->where('tid', $request->tid)->count() > 0) {
return json(trans('user.closet.add.repeated'), 1);
}
$user->closet()->attach($tid, ['item_name' => $request->name]);
$user->setScore(option('score_per_closet_item'), 'minus');
return json(trans('user.closet.add.success', ['name' => $request->input('name')]), 0);
}
public function rename(Request $request)
@ -103,35 +91,34 @@ class ClosetController extends Controller
'new_name' => 'required|no_special_chars',
]);
if ($this->closet->rename($request->tid, $request->new_name)) {
$this->closet->save();
$user = auth()->user();
return json(trans('user.closet.rename.success', ['name' => $request->new_name]), 0);
} else {
if ($user->closet()->where('tid', $request->tid)->count() == 0) {
return json(trans('user.closet.remove.non-existent'), 1);
}
$user->closet()->updateExistingPivot($request->tid, ['item_name' => $request->new_name]);
return json(trans('user.closet.rename.success', ['name' => $request->new_name]), 0);
}
public function remove(Request $request)
{
$this->validate($request, [
'tid' => 'required|integer',
'tid' => 'required|integer',
]);
if ($this->closet->remove($request->tid)) {
$t = Texture::find($request->tid);
$t->likes = $t->likes - 1;
$t->save();
$user = auth()->user();
$this->closet->save();
if (option('return_score')) {
Auth::user()->setScore(option('score_per_closet_item'), 'plus');
}
return json(trans('user.closet.remove.success'), 0);
} else {
if ($user->closet()->where('tid', $request->tid)->count() == 0) {
return json(trans('user.closet.remove.non-existent'), 1);
}
$user->closet()->detach($request->tid);
if (option('return_score')) {
$user->setScore(option('score_per_closet_item'), 'plus');
}
return json(trans('user.closet.remove.success'), 0);
}
}

View File

@ -252,7 +252,7 @@ class SetupController extends Controller
}
$existingTables = [];
$tables = $tables ?: ['users', 'closets', 'players', 'textures', 'options'];
$tables = $tables ?: ['users', 'user_closet', 'players', 'textures', 'options'];
foreach ($tables as $tableName) {
// Table prefix will be added automatically

View File

@ -7,7 +7,6 @@ use Option;
use Session;
use Storage;
use App\Models\User;
use App\Models\Closet;
use App\Models\Player;
use App\Models\Texture;
use Illuminate\Http\Request;
@ -48,7 +47,7 @@ class SkinlibController extends Controller
*/
public function getSkinlibFiltered(Request $request)
{
$currentUser = Auth::user();
$user = Auth::user();
// Available filters: skin, steve, alex, cape
$filter = $request->input('filter', 'skin');
@ -74,7 +73,7 @@ class SkinlibController extends Controller
if ($filter == 'skin') {
$query = Texture::where(function ($innerQuery) {
// Nested condition, DO NOT MODIFY
$innerQuery->where('type', '=', 'steve')->orWhere('type', '=', 'alex');
$innerQuery->where('type', 'steve')->orWhere('type', 'alex');
});
} else {
$query = Texture::where('type', $filter);
@ -88,14 +87,14 @@ class SkinlibController extends Controller
$query = $query->where('uploader', $uploader);
}
if (! $currentUser) {
if (! $user) {
// Show public textures only to anonymous visitors
$query = $query->where('public', true);
} else {
// Show private textures when show uploaded textures of current user
if ($uploader != $currentUser->uid && ! $currentUser->isAdmin()) {
$query = $query->where(function ($innerQuery) use ($currentUser) {
$innerQuery->where('public', true)->orWhere('uploader', '=', $currentUser->uid);
if ($uploader != $user->uid && ! $user->isAdmin()) {
$query = $query->where(function ($innerQuery) use ($user) {
$innerQuery->where('public', true)->orWhere('uploader', '=', $user->uid);
});
}
}
@ -107,16 +106,16 @@ class SkinlibController extends Controller
->take($itemsPerPage)
->get();
if ($currentUser) {
$closet = new Closet($currentUser->uid);
if ($user) {
$closet = $user->closet()->get();
foreach ($textures as $item) {
$item->liked = $closet->has($item->tid);
$item->liked = $closet->contains('tid', $item->tid);
}
}
return response()->json([
'items' => $textures,
'current_uid' => $currentUser ? $currentUser->uid : 0,
'current_uid' => $user ? $user->uid : 0,
'total_pages' => $totalPages,
]);
}
@ -152,7 +151,7 @@ class SkinlibController extends Controller
public function info($tid)
{
if ($t = Texture::find($tid)) {
return json($t->toArray());
return json(array_merge($t->toArray(), ['likes' => $t->likes]));
} else {
return json([]);
}
@ -176,7 +175,6 @@ class SkinlibController extends Controller
$t = new Texture();
$t->name = $request->input('name');
$t->type = $request->input('type');
$t->likes = 1;
$t->hash = bs_hash_file($request->file('file'));
$t->size = ceil($request->file('file')->getSize() / 1024);
$t->public = $request->input('public') == 'true';
@ -212,11 +210,10 @@ class SkinlibController extends Controller
$user->setScore($cost, 'minus');
if ($user->getCloset()->add($t->tid, $t->name)) {
return json(trans('skinlib.upload.success', ['name' => $request->input('name')]), 0, [
'tid' => $t->tid,
]);
}
$user->closet()->attach($t->tid, ['item_name' => $t->name]);
return json(trans('skinlib.upload.success', ['name' => $request->input('name')]), 0, [
'tid' => $t->tid,
]);
}
// @codeCoverageIgnore
@ -281,10 +278,14 @@ class SkinlibController extends Controller
$type = $t->type == 'cape' ? 'cape' : 'skin';
Player::where("tid_$type", $t->tid)
->where('uid', '<>', session('uid'))
->get()
->each(function ($player) use ($type) {
$player->setTexture(["tid_$type" => 0]);
});
->update(["tid_$type" => 0]);
$t->likers()->get()->each(function ($user) use ($t) {
$user->closet()->detach($t->tid);
if (option('return_score')) {
$user->setScore(option('score_per_closet_item'), 'plus');
}
});
@$users->get($t->uploader)->setScore($score_diff, 'plus');

View File

@ -1,268 +0,0 @@
<?php
namespace App\Models;
use DB;
use App\Events;
use Illuminate\Support\Collection;
class Closet
{
public $uid;
/**
* Instance of Query Builder.
*
* @var \Illuminate\Database\Query\Builder
*/
private $db;
/**
* Textures array generated from json.
*
* @var Collection
*/
private $textures;
/**
* Indicates if closet has been modified.
*
* @var array
*/
private $closet_modified = false;
/**
* Construct Closet object with owner's uid.
*
* @param int $uid
*/
public function __construct($uid)
{
$this->uid = $uid;
$this->db = DB::table('closets');
// Create a new closet if not exists
if ($this->db->where('uid', $uid)->count() == 0) {
$this->db->insert([
'uid' => $uid,
'textures' => '[]',
]);
}
// Load items from json string
$this->textures = collect(json_decode(
$this->db->where('uid', $uid)->first()->textures,
true
));
event(new Events\ClosetWillBeFiltered($this));
// Traverse items in the closet
$removedCount = $this->textures->filter(function ($texture) use ($uid) {
$t = Texture::find($texture['tid']);
// If the texture was deleted
if (is_null($t)) {
return true;
}
if (! $t->public && $t->uploader != $uid && ! app('users')->get($uid)->isAdmin()) {
return true;
}
return false;
})->each(function ($texture) use ($uid) {
$this->remove($texture['tid']);
})->count();
event(new Events\ClosetWasFiltered($this));
// Return scores if the texture was deleted or set as private
if (option('return_score')) {
app('users')->get($uid)->setScore(
option('score_per_closet_item') * $removedCount,
'plus'
);
}
}
/**
* Get array of instances of App\Models\Texture.
*
* @param string $category "skin" or "cape" or "all".
* @return array
*/
public function getItems($category = 'all')
{
$textures = Texture::whereIn('tid', $this->textures->pluck('tid')->all())
->get()
->map(function ($texture) {
$in_closet = $this->textures
->where('tid', $texture->tid)
->first();
return [
'tid' => $texture->tid,
'name' => $in_closet['name'],
'type' => $texture->type,
'add_at' => $in_closet['add_at'],
];
})
->sortByDesc('add_at');
if ($category == 'all') {
return $textures->values()->all();
} elseif ($category == 'cape') {
return $textures->filter(function ($texture) {
return $texture['type'] == 'cape';
})->values();
} else {
return $textures->reject(function ($texture) {
return $texture['type'] == 'cape';
})->values();
}
}
/**
* Add an item to the closet.
*
* @param int $tid
* @param string $name
* @return bool
*/
public function add($tid, $name)
{
if ($this->has($tid)) {
return false;
}
$this->textures->push([
'tid' => (int) $tid,
'name' => $name,
'add_at' => time(),
]);
$this->closet_modified = true;
return true;
}
/**
* Check if texture is in the closet.
*
* @param int $tid
* @return bool
*/
public function has($tid)
{
return $this->textures->contains('tid', $tid);
}
/**
* Get one texture info.
*
* @param int $tid
* @return array|null Result
*/
public function get($tid)
{
return $this->textures->where('tid', $tid)->first();
}
/**
* Rename closet item.
*
* @param int $tid
* @param string $newName
* @return bool
*/
public function rename($tid, $newName)
{
if (! $this->has($tid)) {
return false;
}
$this->textures->transform(function ($texture) use ($tid, $newName) {
if ($texture['tid'] == $tid) {
$texture['name'] = $newName;
}
return $texture;
});
$this->closet_modified = true;
return true;
}
/**
* Remove a texture from closet.
*
* @param int $tid
* @return bool
*/
public function remove($tid)
{
if (! $this->has($tid)) {
return false;
}
$this->textures = $this->textures->reject(function ($texture) use ($tid) {
return $texture['tid'] == $tid;
});
$this->closet_modified = true;
return true;
}
/**
* Set textures string manually.
*
* @param string $textures
* @return int
*/
public function setTextures($textures)
{
return $this->db->where('uid', $this->uid)->update(['textures' => $textures]);
}
/**
* Do really database operations.
*
* @return bool
*/
public function save()
{
if (! $this->closet_modified) {
return false;
}
$this->closet_modified = false;
return $this->setTextures($this->textures->values()->toJson());
}
/**
* Save when the object will be destructed.
*
* @return void
*/
public function __destruct()
{
$this->save();
}
/**
* Get all closets.
*
* @return array
*/
public static function all()
{
$result = [];
foreach (DB::table('closets')->pluck('uid') as $uid) {
$result[] = new Closet($uid);
}
return $result;
}
}

View File

@ -16,14 +16,23 @@ class Texture extends Model
*/
protected $casts = [
'tid' => 'integer',
'likes' => 'integer',
'size' => 'integer',
'uploader' => 'integer',
'public' => 'boolean',
];
public function getLikesAttribute()
{
return $this->likers()->count();
}
public function scopeLike($query, $field, $value)
{
return $query->where($field, 'LIKE', "%$value%");
}
public function likers()
{
return $this->belongsToMany(User::class, 'user_closet')->withPivot('item_name');
}
}

View File

@ -18,12 +18,6 @@ class User extends Authenticatable
const ADMIN = 1;
const SUPER_ADMIN = 2;
/**
* Instance of Closet.
* @var \App\Models\Closet
*/
protected $closet;
/**
* Properties for Eloquent Model.
*/
@ -61,18 +55,9 @@ class User extends Authenticatable
return $this->permission >= static::ADMIN;
}
/**
* Get closet instance.
*
* @return \App\Models\Closet
*/
public function getCloset()
public function closet()
{
if (! $this->closet) {
$this->closet = new Closet($this->uid);
}
return $this->closet;
return $this->belongsToMany(Texture::class, 'user_closet')->withPivot('item_name');
}
/**
@ -332,11 +317,7 @@ class User extends Authenticatable
*/
public function delete()
{
// Delete the players he owned
Player::where('uid', $this->uid)->delete();
// Delete his closet
DB::table('closets')->where('uid', $this->uid)->delete();
return parent::delete();
}

View File

@ -1,11 +0,0 @@
<?php
use App\Models\User;
use App\Models\Closet;
$factory->define(Closet::class, function (Faker\Generator $faker) {
return [
'uid' => factory(User::class)->create()->uid,
'textures' => '[]',
];
});

View File

@ -6,7 +6,6 @@ $factory->define(Texture::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstName,
'type' => 'steve',
'likes' => rand(0, 50),
'hash' => $faker->sha256,
'size' => rand(1, 2048),
'uploader' => factory(App\Models\User::class)->create()->uid,
@ -19,7 +18,6 @@ $factory->defineAs(Texture::class, 'alex', function (Faker\Generator $faker) {
return [
'name' => $faker->firstName,
'type' => 'alex',
'likes' => rand(0, 50),
'hash' => $faker->sha256,
'size' => rand(1, 2048),
'uploader' => factory(App\Models\User::class)->create()->uid,
@ -32,7 +30,6 @@ $factory->defineAs(Texture::class, 'cape', function (Faker\Generator $faker) {
return [
'name' => $faker->firstName,
'type' => 'cape',
'likes' => rand(0, 50),
'hash' => $faker->sha256,
'size' => rand(1, 2048),
'uploader' => factory(App\Models\User::class)->create()->uid,

View File

@ -25,11 +25,6 @@ class CreateAllTables extends Migration
$table->dateTime('register_at');
});
Schema::create('closets', function (Blueprint $table) {
$table->increments('uid');
$table->longText('textures');
});
Schema::create('players', function (Blueprint $table) {
$table->increments('pid');
$table->integer('uid');
@ -42,7 +37,6 @@ class CreateAllTables extends Migration
$table->increments('tid');
$table->string('name', 50);
$table->string('type', 10);
$table->integer('likes');
$table->string('hash', 64);
$table->integer('size');
$table->integer('uploader');
@ -65,7 +59,6 @@ class CreateAllTables extends Migration
public function down()
{
Schema::drop('users');
Schema::drop('closets');
Schema::drop('players');
Schema::drop('textures');
Schema::drop('options');

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCloset extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_closet', function (Blueprint $table) {
$table->integer('user_uid');
$table->integer('texture_tid');
$table->text('item_name')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_closet');
}
}

View File

@ -47,7 +47,7 @@ Object.defineProperty(blessing, 'extra', {
download: {{ option('allow_downloading_texture') ? 'true' : 'false' }},
currentUid: {{ is_null($user) ? '0' : $user->uid }},
admin: {{ $user && $user->isAdmin() ? 'true' : 'false' }},
inCloset: {{ $user && $user->getCloset()->has($texture->tid) ? 'true' : 'false' }},
inCloset: {{ $user && $user->closet()->where('tid', $texture->tid)->count() > 0 ? 'true' : 'false' }},
nickname: @php echo ($up = app('users')->get($texture->uploader)) ? '"'.$up->nickname.'"' : 'null' @endphp
})
})

View File

@ -3,7 +3,6 @@
namespace Tests;
use App\Models\User;
use App\Models\Closet;
use App\Models\Texture;
use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -31,18 +30,16 @@ class ClosetControllerTest extends TestCase
public function testGetClosetData()
{
$textures = factory(Texture::class, 10)->create();
$closet = new Closet($this->user->uid);
$textures->each(function ($texture) use ($closet) {
$closet->add($texture->tid, $texture->name);
$textures->each(function ($t) {
$this->user->closet()->attach($t->tid, ['item_name' => $t->name]);
});
$closet->save();
// Use default query parameters
$this->getJson('/user/closet-data')
->assertJsonStructure([
'category',
'total_pages',
'items' => [['tid', 'name', 'type', 'add_at']],
'items' => [['tid', 'name', 'type']],
]);
// Responsive
@ -55,8 +52,7 @@ class ClosetControllerTest extends TestCase
// Get capes
$cape = factory(Texture::class, 'cape')->create();
$closet->add($cape->tid, 'custom_name');
$closet->save();
$this->user->closet()->attach($cape->tid, ['item_name' => 'custom_name']);
$this->getJson('/user/closet-data?category=cape')
->assertJson([
'category' => 'cape',
@ -65,7 +61,6 @@ class ClosetControllerTest extends TestCase
'tid' => $cape->tid,
'name' => 'custom_name',
'type' => 'cape',
'add_at' => $closet->get($cape->tid)['add_at'],
]],
]);
@ -79,7 +74,6 @@ class ClosetControllerTest extends TestCase
'tid' => $random->tid,
'name' => $random->name,
'type' => $random->type,
'add_at' => $closet->get($random->tid)['add_at'],
]],
]);
}
@ -87,6 +81,7 @@ class ClosetControllerTest extends TestCase
public function testAdd()
{
$texture = factory(Texture::class)->create();
$likes = $texture->likes;
$name = 'my';
option(['score_per_closet_item' => 10]);
@ -152,11 +147,10 @@ class ClosetControllerTest extends TestCase
'errno' => 0,
'msg' => trans('user.closet.add.success', ['name' => $name]),
]);
$this->assertEquals($texture->likes + 1, Texture::find($texture->tid)->likes);
$this->assertEquals($likes + 1, Texture::find($texture->tid)->likes);
$this->user = User::find($this->user->uid);
$this->assertEquals(90, $this->user->score);
$closet = new Closet($this->user->uid);
$this->assertTrue($closet->has($texture->tid));
$this->assertEquals(1, $this->user->closet()->count());
// If the texture is duplicated, should be warned
$this->postJson(
@ -217,10 +211,7 @@ class ClosetControllerTest extends TestCase
]);
// Rename a closet item successfully
$closet = new Closet($this->user->uid);
$closet->add($texture->tid, 'name');
$closet->save();
$closet = new Closet($this->user->uid);
$this->user->closet()->attach($texture->tid, ['item_name' => 'name']);
$this->postJson(
'/user/closet/rename',
['tid' => $texture->tid, 'new_name' => $name]
@ -228,14 +219,13 @@ class ClosetControllerTest extends TestCase
'errno' => 0,
'msg' => trans('user.closet.rename.success', ['name' => 'new']),
]);
$closet->save();
$closet = new Closet($this->user->uid);
$this->assertFalse(collect($closet->getItems())->where('name', 'new')->isEmpty());
$this->assertEquals(1, $this->user->closet()->where('item_name', 'new')->count());
}
public function testRemove()
{
$texture = factory(Texture::class)->create();
$likes = $texture->likes;
// Missing `tid` field
$this->postJson('/user/closet/remove')
@ -263,9 +253,7 @@ class ClosetControllerTest extends TestCase
]);
// Should return score if `return_score` is true
$closet = new Closet($this->user->uid);
$closet->add($texture->tid, 'name');
$closet->save();
$this->user->closet()->attach($texture->tid, ['item_name' => 'name']);
$score = $this->user->score;
$this->postJson(
'/user/closet/remove',
@ -274,17 +262,15 @@ class ClosetControllerTest extends TestCase
'errno' => 0,
'msg' => trans('user.closet.remove.success'),
]);
$closet = new Closet($this->user->uid);
$this->assertEquals($texture->likes - 1, Texture::find($texture->tid)->likes);
$this->assertEquals($likes, Texture::find($texture->tid)->likes);
$this->assertEquals($score + option('score_per_closet_item'), $this->user->score);
$this->assertFalse($closet->has($texture->tid));
$this->assertEquals(0, $this->user->closet()->count());
$texture = Texture::find($texture->tid);
$likes = $texture->likes;
// Should not return score if `return_score` is false
option(['return_score' => false]);
$closet = new Closet($this->user->uid);
$closet->add($texture->tid, 'name');
$closet->save();
$this->user->closet()->attach($texture->tid, ['item_name' => 'name']);
$score = $this->user->score;
$this->postJson(
'/user/closet/remove',
@ -293,9 +279,8 @@ class ClosetControllerTest extends TestCase
'errno' => 0,
'msg' => trans('user.closet.remove.success'),
]);
$closet = new Closet($this->user->uid);
$this->assertEquals($texture->likes - 1, Texture::find($texture->tid)->likes);
$this->assertEquals($likes, Texture::find($texture->tid)->likes);
$this->assertEquals($score, $this->user->score);
$this->assertFalse($closet->has($texture->tid));
$this->assertEquals(0, $this->user->closet()->count());
}
}

View File

@ -109,7 +109,7 @@ class MiddlewareTest extends TestCase
$this->get('/setup')->assertSee('Already installed');
$tables = [
'closets', 'migrations', 'options', 'players', 'textures', 'users',
'user_closet', 'migrations', 'options', 'players', 'textures', 'users',
];
array_walk($tables, function ($table) {
Schema::dropIfExists($table);

View File

@ -1,51 +0,0 @@
<?php
namespace Tests;
use App\Models\User;
use App\Models\Closet;
use App\Models\Texture;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ClosetTest extends TestCase
{
use DatabaseTransactions;
public function testAll()
{
for ($i = 0; $i < 2; $i++) {
$user = factory(User::class)->create();
(new Closet($user->uid))->save();
}
$this->assertCount(2, Closet::all());
}
public function testFilterInvalidTexture()
{
$other = factory(User::class)->create();
$texture = factory(Texture::class)->create([
'uploader' => $other->uid,
'public' => false,
]);
$user = factory(User::class)->create();
$closet = new Closet($user->uid);
$closet->add(-1, '');
$closet->add($texture->tid, '');
$closet->save();
$this->assertCount(0, (new Closet($user->uid))->getItems());
$this->assertEquals(
$user->score + 2 * option('score_per_closet_item'),
User::find($user->uid)->score
);
option(['return_score' => false]);
$closet = new Closet($user->uid);
$closet->add(-1, '');
$closet->add($texture->tid, '');
$closet->save();
$user = User::find($user->uid);
$this->assertCount(0, (new Closet($user->uid))->getItems());
$this->assertEquals($user->score, User::find($user->uid)->score);
}
}

View File

@ -33,7 +33,7 @@ class SetupControllerTest extends TestCase
protected function dropAllTables()
{
$tables = [
'closets', 'migrations', 'options', 'players', 'textures', 'users',
'user_closet', 'migrations', 'options', 'players', 'textures', 'users',
];
array_walk($tables, function ($table) {
Schema::dropIfExists($table);

View File

@ -3,7 +3,6 @@
namespace Tests;
use App\Models\User;
use App\Models\Closet;
use App\Models\Player;
use App\Models\Texture;
use Illuminate\Support\Str;
@ -15,25 +14,6 @@ class SkinlibControllerTest extends TestCase
{
use DatabaseTransactions;
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,
'upload_at' => $texture->upload_at->format('Y-m-d H:i:s'),
];
})
->all();
}
public function testIndex()
{
$this->get('/skinlib')->assertViewHas('user');
@ -54,26 +34,28 @@ class SkinlibControllerTest extends TestCase
$capes = factory(Texture::class, 'cape')->times(5)->create();
// Default arguments
$expected = $skins
->sortByDesc('upload_at')
->values(); // WTF! DO NOT FORGET IT!!
$this->getJson('/skinlib/data')
$items = $this->getJson('/skinlib/data')
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$this->assertCount(10, $items);
$this->assertTrue(collect($items)->every(function ($item) {
return $item['type'] == 'steve' || $item['type'] == 'alex';
}));
// Only steve
$expected = $steves
->sortByDesc('upload_at')
->values();
$this->getJson('/skinlib/data?filter=steve')
$items = $this->getJson('/skinlib/data?filter=steve')
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$this->assertCount(5, $items);
$this->assertTrue(collect($items)->every(function ($item) {
return $item['type'] == 'steve';
}));
// Invalid type
$this->getJson('/skinlib/data?filter=what')
@ -84,70 +66,87 @@ class SkinlibControllerTest extends TestCase
]);
// Only capes
$expected = $capes
->sortByDesc('upload_at')
->values();
$this->getJson('/skinlib/data?filter=cape')
$items = $this->getJson('/skinlib/data?filter=cape')
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$this->assertCount(5, $items);
$this->assertTrue(collect($items)->every(function ($item) {
return $item['type'] == 'cape';
}));
// Only specified uploader
$uid = $skins->random()->uploader;
$expected = $skins
$owned = $skins
->filter(function ($texture) use ($uid) {
return $texture->uploader == $uid;
})
->sortByDesc('upload_at')
->values();
$this->getJson('/skinlib/data?uploader='.$uid)
});
$items = $this->getJson('/skinlib/data?uploader='.$uid)
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$this->assertCount($owned->count(), $items);
$this->assertTrue(collect($items)->every(function ($item) use ($uid) {
return $item['uploader'] == $uid;
}));
// Sort by `tid`
$this->getJson('/skinlib/data?sort=tid')
$ordered = $skins->sortByDesc('tid')->map(function ($skin) {
return $skin->tid;
})->values();
$items = $this->getJson('/skinlib/data?sort=tid')
->assertJson([
'items' => $this->serializeTextures($skins->sortByDesc('tid')->values()),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertArraySubset($ordered, $items);
// Search
$keyword = Str::limit($skins->random()->name, 1, '');
$expected = $skins
$keyworded = $skins
->filter(function ($texture) use ($keyword) {
return Str::contains($texture->name, $keyword) ||
Str::contains($texture->name, strtolower($keyword));
})
->sortByDesc('upload_at')
->values();
$this->getJson('/skinlib/data?keyword='.$keyword)
});
$items = $this->getJson('/skinlib/data?keyword='.$keyword)
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$this->assertCount($keyworded->count(), $items);
// More than one argument
$keyword = Str::limit($skins->random()->name, 1, '');
$expected = $skins
$filtered = $skins
->filter(function ($texture) use ($keyword) {
return Str::contains($texture->name, $keyword) ||
Str::contains($texture->name, strtolower($keyword));
})
->sortByDesc('likes')
->sortByDesc('size')
->map(function ($skin) {
return $skin->tid;
})
->values();
$this->getJson('/skinlib/data?sort=likes&keyword='.$keyword)
$items = $this->getJson('/skinlib/data?sort=size&keyword='.$keyword)
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 1,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertCount($filtered->count(), $items);
$this->assertArraySubset($filtered, $items);
// Pagination
$steves = factory(Texture::class)
@ -155,58 +154,88 @@ class SkinlibControllerTest extends TestCase
->create()
->merge($steves);
$skins = $steves->merge($alexs);
$expected = $skins
$page1 = $skins
->sortByDesc('upload_at')
->values()
->map(function ($skin) {
return $skin->tid;
})
->forPage(1, 20);
$expected = $this->serializeTextures($expected);
$this->getJson('/skinlib/data')
$items = $this->getJson('/skinlib/data')
->assertJson([
'items' => $expected,
'current_uid' => 0,
'total_pages' => 2,
]);
$this->getJson('/skinlib/data?page=-5')
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertCount(20, $items);
$this->assertArraySubset($page1, $items);
$items = $this->getJson('/skinlib/data?page=-5')
->assertJson([
'items' => $expected,
'current_uid' => 0,
'total_pages' => 2,
]);
$expected = $skins
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertCount(20, $items);
$this->assertArraySubset($page1, $items);
$page2 = $skins
->sortByDesc('upload_at')
->values()
->map(function ($skin) {
return $skin->tid;
})
->forPage(2, 20)
->values();
$expected = $this->serializeTextures($expected);
$this->getJson('/skinlib/data?page=2')
$items = $this->getJson('/skinlib/data?page=2')
->assertJson([
'items' => $expected,
'current_uid' => 0,
'total_pages' => 2,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertCount(5, $items);
$this->assertArraySubset($page2, $items);
$this->getJson('/skinlib/data?page=8')
->assertJson([
'items' => [],
'current_uid' => 0,
'total_pages' => 2,
]);
$this->getJson('/skinlib/data?items_per_page=-6&page=2')
$items = $this->getJson('/skinlib/data?items_per_page=-6&page=2')
->assertJson([
'items' => $expected,
'current_uid' => 0,
'total_pages' => 2,
]);
$expected = $skins
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertCount($page2->count(), $items);
$this->assertArraySubset($page2, $items);
$page3 = $skins
->sortByDesc('upload_at')
->values()
->map(function ($skin) {
return $skin->tid;
})
->forPage(3, 8)
->values();
$this->getJson('/skinlib/data?page=3&items_per_page=8')
$items = $this->getJson('/skinlib/data?page=3&items_per_page=8')
->assertJson([
'items' => $this->serializeTextures($expected),
'current_uid' => 0,
'total_pages' => 4,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertCount($page3->count(), $items);
$this->assertArraySubset($page3, $items);
// Add some private textures
$uploader = factory(User::class)->create();
@ -216,85 +245,82 @@ class SkinlibControllerTest extends TestCase
->create(['public' => false, 'uploader' => $uploader->uid]);
// If not logged in, private textures should not be shown
$expected = $skins
$paged = $skins
->sortByDesc('upload_at')
->map(function ($skin) {
return $skin->tid;
})
->values()
->forPage(1, 20);
$expected = $this->serializeTextures($expected);
$this->getJson('/skinlib/data')
$items = $this->getJson('/skinlib/data')
->assertJson([
'items' => $expected,
'current_uid' => 0,
'total_pages' => 2,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertArraySubset($paged, $items);
// Other users should not see someone's private textures
for ($i = 0, $length = count($expected); $i < $length; $i++) {
$expected[$i]['liked'] = false;
}
$this->actAs($otherUser)
$items = $this->actAs($otherUser)
->getJson('/skinlib/data')
->assertJson([
'items' => $expected,
'current_uid' => $otherUser->uid,
'total_pages' => 2,
]);
])
->decodeResponseJson('items');
$this->assertTrue(collect($items)->every(function ($item) {
return !$item['liked'];
}));
// 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, $length = count($expected); $i < $length; $i++) {
if ($expected[$i]['tid'] == $texture->tid) {
$expected[$i]['liked'] = true;
} else {
$expected[$i]['liked'] = false;
}
}
$texture = $skins->sortByDesc('upload_at')->values()->first();
$otherUser->closet()->attach($texture->tid, ['item_name' => $texture->name]);
$this->getJson('/skinlib/data')
->assertJson([
'items' => $expected,
'items' => [
['tid' => $texture->tid, 'liked' => true]
],
'current_uid' => $otherUser->uid,
'total_pages' => 2,
]);
// Uploader can see his private textures
$expected = $skins
$withPrivate = $skins
->merge($private)
->sortByDesc('upload_at')
->map(function ($skin) {
return $skin->tid;
})
->values()
->forPage(1, 20);
$expected = $this->serializeTextures($expected);
for ($i = 0, $length = count($expected); $i < $length; $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)
$items = $this->actAs($uploader)
->getJson('/skinlib/data')
->assertJson([
'items' => $expected,
'current_uid' => $uploader->uid,
'total_pages' => 2,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertArraySubset($withPrivate, $items);
// Administrators can see private textures
$admin = factory(User::class, 'admin')->create();
$this->actAs($admin)
$items = $this->actAs($admin)
->getJson('/skinlib/data')
->assertJson([
'items' => $expected,
'current_uid' => $admin->uid,
'total_pages' => 2,
]);
])
->decodeResponseJson('items');
$items = array_map(function ($item) {
return $item['tid'];
}, $items);
$this->assertArraySubset($withPrivate, $items);
}
public function testShow()
@ -769,6 +795,8 @@ class SkinlibControllerTest extends TestCase
$uploader->score += $texture->size * option('private_score_per_storage');
$uploader->save();
$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])
->assertJson([
'errno' => 0,
@ -776,6 +804,26 @@ class SkinlibControllerTest extends TestCase
'public' => false,
]);
$this->assertEquals(0, Player::find($player->pid)->tid_skin);
$this->assertEquals(0, $other->closet()->count());
$this->assertEquals(
$other->score + option('score_per_closet_item'),
User::find($other->uid)->score
);
// Without returning score
option(['return_score' => false]);
$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])
->assertJson([
'errno' => 0,
'public' => false,
]);
$this->assertEquals(
$other->score,
User::find($other->uid)->score
);
}
public function testRename()