extract "single-player" function as plugin

This commit is contained in:
Pig Fang 2020-06-03 14:47:44 +08:00
parent f4c2da3b31
commit 7ae9a05f0c
18 changed files with 6 additions and 457 deletions

View File

@ -6,7 +6,6 @@ use App\Events\PlayerWasAdded;
use App\Events\PlayerWasDeleted; use App\Events\PlayerWasDeleted;
use App\Events\PlayerWillBeAdded; use App\Events\PlayerWillBeAdded;
use App\Events\PlayerWillBeDeleted; use App\Events\PlayerWillBeDeleted;
use App\Http\Middleware\CheckPlayerOwner;
use App\Models\Player; use App\Models\Player;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
@ -77,10 +76,6 @@ class PlayerController extends Controller
/** @var User */ /** @var User */
$user = Auth::user(); $user = Auth::user();
if (option('single_player', false)) {
return json(trans('user.player.add.single'), 1);
}
$name = $request->validate([ $name = $request->validate([
'name' => [ 'name' => [
'required', 'required',
@ -138,10 +133,6 @@ class PlayerController extends Controller
return json($can->getReason(), 1); return json($can->getReason(), 1);
} }
if (option('single_player', false)) {
return json(trans('user.player.delete.single'), 1);
}
$dispatcher->dispatch('player.deleting', [$player, $user]); $dispatcher->dispatch('player.deleting', [$player, $user]);
event(new PlayerWillBeDeleted($player)); event(new PlayerWillBeDeleted($player));
@ -186,13 +177,6 @@ class PlayerController extends Controller
$player->name = $name; $player->name = $name;
$player->save(); $player->save();
if (option('single_player', false)) {
/** @var User */
$user = auth()->user();
$user->nickname = $name;
$user->save();
}
$dispatcher->dispatch('player.renamed', [$player, $old]); $dispatcher->dispatch('player.renamed', [$player, $old]);
return json( return json(
@ -262,41 +246,4 @@ class PlayerController extends Controller
return json(trans('user.player.clear.success', ['name' => $player->name]), 0, $player->toArray()); return json(trans('user.player.clear.success', ['name' => $player->name]), 0, $player->toArray());
} }
public function bind(Request $request, Dispatcher $dispatcher)
{
$name = $request->validate([
'player' => [
'required',
new Rules\PlayerName(),
'min:'.option('player_name_length_min'),
'max:'.option('player_name_length_max'),
],
])['player'];
/** @var User */
$user = Auth::user();
$player = Player::where('name', $name)->first();
if (empty($player)) {
$dispatcher->dispatch('player.adding', [$name, $user]);
event(new PlayerWillBeAdded($name));
$player = new Player();
$player->uid = $user->uid;
$player->name = $name;
$player->tid_skin = 0;
$player->save();
$dispatcher->dispatch('player.added', [$player, $user]);
event(new PlayerWasAdded($player));
} elseif ($player->uid != $user->uid) {
return json(trans('user.player.rename.repeated'), 1);
}
$user->players()->where('name', '<>', $name)->delete();
$user->nickname = $name;
$user->save();
return json(trans('user.player.bind.success'), 0);
}
} }

View File

@ -56,13 +56,13 @@ class PlayersManagementController extends Controller
], ],
])['player_name']; ])['player_name'];
$dispatcher->dispatch('player.name.updating', [$player, $name]); $dispatcher->dispatch('player.renaming', [$player, $name]);
$oldName = $player->name; $oldName = $player->name;
$player->name = $name; $player->name = $name;
$player->save(); $player->save();
$dispatcher->dispatch('player.name.updated', [$player, $oldName]); $dispatcher->dispatch('player.renamed', [$player, $oldName]);
return json(trans('admin.players.name.success', ['player' => $player->name]), 0); return json(trans('admin.players.name.success', ['player' => $player->name]), 0);
} }

View File

@ -217,10 +217,6 @@ class UserController extends Controller
switch ($action) { switch ($action) {
case 'nickname': case 'nickname':
if (option('single_player', false)) {
return json(trans('user.profile.nickname.single'), 1);
}
$request->validate(['new_nickname' => 'required']); $request->validate(['new_nickname' => 'required']);
$nickname = $request->input('new_nickname'); $nickname = $request->input('new_nickname');

View File

@ -1,40 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
class RequireBindPlayer
{
public function handle($request, Closure $next)
{
if (!option('single_player', false)) {
if ($request->is('user/player/bind')) {
return redirect('/user');
} else {
return $next($request);
}
}
// This allows us to fetch players list.
if ($request->is('user/player/list')) {
return $next($request);
}
$count = auth()->user()->players()->count();
if ($request->is('user/player/bind')) {
if ($count == 1) {
return redirect('/user');
} else {
return $next($request);
}
}
if ($count == 1) {
return $next($request);
} else {
return redirect('user/player/bind');
}
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Listeners\SinglePlayer;
use App\Models\Player;
class UpdateOwnerNickName
{
/**
* @param Player $player
*/
public function handle($player)
{
$owner = $player->user;
if (option('single_player', false) && $owner) {
$owner->nickname = $player->name;
$owner->save();
}
}
}

View File

@ -33,9 +33,6 @@ class EventServiceProvider extends ServiceProvider
'App\Events\RenderingHeader' => [ 'App\Events\RenderingHeader' => [
Listeners\SerializeGlobals::class, Listeners\SerializeGlobals::class,
], ],
'player.name.updated' => [
Listeners\SinglePlayer\UpdateOwnerNickName::class,
],
'auth.registration.completed' => [ 'auth.registration.completed' => [
Listeners\SendEmailVerification::class, Listeners\SendEmailVerification::class,
], ],

View File

@ -29,11 +29,6 @@ export default [
</div> </div>
), ),
}, },
{
path: 'user/player/bind',
react: () => import('../views/user/BindPlayers'),
el: 'form',
},
{ {
path: 'user/profile', path: 'user/profile',
module: [() => import('../views/user/profile/index')], module: [() => import('../views/user/profile/index')],

View File

@ -37,8 +37,8 @@ export default {
user: { user: {
home: () => '/user' as const, home: () => '/user' as const,
closet: { closet: {
add: () => '/user/closet' as const,
page: () => '/user/closet' as const, page: () => '/user/closet' as const,
add: () => '/user/closet' as const,
ids: () => '/user/closet/ids' as const, ids: () => '/user/closet/ids' as const,
list: () => '/user/closet/list' as const, list: () => '/user/closet/list' as const,
rename: (tid: number) => `/user/closet/${tid}`, rename: (tid: number) => `/user/closet/${tid}`,
@ -48,7 +48,6 @@ export default {
player: { player: {
add: () => '/user/player' as const, add: () => '/user/player' as const,
page: () => '/user/player' as const, page: () => '/user/player' as const,
bind: () => '/user/player/bind' as const,
list: () => '/user/player/list' as const, list: () => '/user/player/list' as const,
delete: (player: number) => `/user/player/${player}`, delete: (player: number) => `/user/player/${player}`,
rename: (player: number) => `/user/player/${player}/name`, rename: (player: number) => `/user/player/${player}/name`,

View File

@ -1,98 +0,0 @@
import React, { useState, useEffect } from 'react'
import { hot } from 'react-hot-loader/root'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal } from '@/scripts/notify'
import { Player } from '@/scripts/types'
import Loading from '@/components/Loading'
const BindPlayers: React.FC = () => {
const [players, setPlayers] = useState<string[]>([])
const [selected, setSelected] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isPending, setIsPending] = useState(false)
useEffect(() => {
const getPlayers = async () => {
setIsLoading(true)
const response = await fetch.get<fetch.ResponseBody<Player[]>>(
'/user/player/list',
)
const players = response.data.map((player) => player.name)
setPlayers(players)
setSelected(players[0])
setIsLoading(false)
}
getPlayers()
}, [])
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setIsPending(true)
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/user/player/bind',
{ player: selected },
)
if (code === 0) {
await showModal({ mode: 'alert', text: message })
window.location.href = `${blessing.base_url}/user`
} else {
showModal({ mode: 'alert', text: message })
}
setIsPending(false)
}
return isLoading ? (
<Loading />
) : (
<form method="post" onSubmit={handleSubmit}>
{players.length > 0 ? (
<>
<p>{t('user.bindExistedPlayer')}</p>
<div className="mb-3">
{players.map((player) => (
<label className="d-block mb-1">
<input
key={player}
type="radio"
className="mr-2"
checked={selected === player}
onChange={() => setSelected(player)}
/>
{player}
</label>
))}
</div>
</>
) : (
<>
<p>{t('user.bindNewPlayer')}</p>
<input
type="text"
className="form-control mb-3"
placeholder={t('general.player.player-name')}
onChange={(e) => setSelected(e.target.value)}
/>
</>
)}
<button
className="btn btn-primary float-right"
type="submit"
disabled={isPending}
>
{isPending ? (
<>
<i className="fas fa-spinner fa-spin mr-1"></i>
{t('general.wait')}
</>
) : (
t('general.submit')
)}
</button>
</form>
)
}
export default hot(BindPlayers)

View File

@ -1,82 +0,0 @@
import React from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import BindPlayers from '@/views/user/BindPlayers'
jest.mock('@/scripts/net')
test('loading indicator', () => {
fetch.get.mockResolvedValue({ data: [] })
const { queryByTitle } = render(<BindPlayers />)
expect(queryByTitle('Loading...')).toBeInTheDocument()
})
describe('submit', () => {
it('have existed players', async () => {
fetch.get.mockResolvedValue({
data: [{ name: 'kumiko' }, { name: 'reina' }],
})
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByLabelText, queryByText } = render(<BindPlayers />)
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
fireEvent.click(getByLabelText('reina'))
fireEvent.click(getByText(t('general.submit')))
await waitFor(() =>
expect(fetch.post).toBeCalledWith('/user/player/bind', {
player: 'reina',
}),
)
expect(queryByText('success')).toBeInTheDocument()
fireEvent.click(getByText(t('general.confirm')))
})
it('no existed players', async () => {
fetch.get.mockResolvedValue({ data: [] })
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByText, getByPlaceholderText, queryByText } = render(
<BindPlayers />,
)
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
fireEvent.input(getByPlaceholderText(t('general.player.player-name')), {
target: { value: 'kumiko' },
})
fireEvent.click(getByText(t('general.submit')))
await waitFor(() =>
expect(fetch.post).toBeCalledWith('/user/player/bind', {
player: 'kumiko',
}),
)
expect(queryByText('success')).toBeInTheDocument()
fireEvent.click(getByText(t('general.confirm')))
})
it('failed', async () => {
fetch.get.mockResolvedValue({ data: [] })
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByPlaceholderText, queryByText } = render(
<BindPlayers />,
)
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
fireEvent.input(getByPlaceholderText(t('general.player.player-name')), {
target: { value: 'kumiko' },
})
fireEvent.click(getByText(t('general.submit')))
await waitFor(() =>
expect(fetch.post).toBeCalledWith('/user/player/bind', {
player: 'kumiko',
}),
)
expect(queryByText('failed')).toBeInTheDocument()
fireEvent.click(getByText(t('general.confirm')))
})
})

View File

@ -128,8 +128,6 @@ user:
typeToSearch: Type to search typeToSearch: Type to search
useAs: Apply... useAs: Apply...
resetSelected: Clear selected resetSelected: Clear selected
bindNewPlayer: You're required to create a player to go ahead. This player will be bound with your account.
bindExistedPlayer: You're required to select a player to go ahead. This player will be bound with your account. Other players will be deleted.
closet: closet:
upload: Upload Texture upload: Upload Texture
use-as: use-as:

View File

@ -87,11 +87,9 @@ player:
add: add:
repeated: The player name is already registered. repeated: The player name is already registered.
lack-score: You don't have enough score to add a player. lack-score: You don't have enough score to add a player.
single: You must own exactly ONE player so you can't add more.
success: Player :name was added successfully. success: Player :name was added successfully.
delete: delete:
single: You must own exactly ONE player so you can't delete it.
success: Player :name was deleted successfully. success: Player :name was deleted successfully.
rename: rename:
@ -104,10 +102,6 @@ player:
clear: clear:
success: The textures of player :name was resetted successfully. success: The textures of player :name was resetted successfully.
bind:
title: Bind Players
success: Bound successfully!
profile: profile:
avatar: avatar:
title: Change Avatar? title: Change Avatar?
@ -128,7 +122,6 @@ profile:
nickname: nickname:
title: Change Nickname title: Change Nickname
empty: No nickname is set now. empty: No nickname is set now.
single: You're not allowed to update nickname, because we've bound your player with your account.
success: Nickname is successfully updated to :nickname success: Nickname is successfully updated to :nickname
email: email:

View File

@ -1,10 +0,0 @@
{% extends 'auth.base' %}
{% block title %}{{ trans('user.player.bind.title') }}{% endblock %}
{% block content %}
<p class="login-box-msg">
<b>{{ trans('user.player.bind.title') }}</b>
</p>
<form></form>
{% endblock %}

View File

@ -48,7 +48,7 @@ Route::prefix('auth')->name('auth.')->group(function () {
Route::prefix('user') Route::prefix('user')
->name('user.') ->name('user.')
->middleware(['authorize', Middleware\RequireBindPlayer::class]) ->middleware(['authorize'])
->group(function () { ->group(function () {
Route::get('', 'UserController@index')->name('home'); Route::get('', 'UserController@index')->name('home');
Route::get('notifications/{id}', 'NotificationsController@read')->name('notification'); Route::get('notifications/{id}', 'NotificationsController@read')->name('notification');
@ -76,8 +76,6 @@ Route::prefix('user')
Route::delete('{player}/textures', 'PlayerController@clearTexture')->name('clear'); Route::delete('{player}/textures', 'PlayerController@clearTexture')->name('clear');
Route::put('{player}/name', 'PlayerController@rename')->name('rename'); Route::put('{player}/name', 'PlayerController@rename')->name('rename');
Route::delete('{player}', 'PlayerController@delete')->name('delete'); Route::delete('{player}', 'PlayerController@delete')->name('delete');
Route::view('bind', 'user.bind')->name('bind');
Route::post('bind', 'PlayerController@bind')->name('bind');
}); });
Route::prefix('closet')->name('closet.')->group(function () { Route::prefix('closet')->name('closet.')->group(function () {

View File

@ -158,14 +158,6 @@ class PlayerControllerTest extends TestCase
$score - option('score_per_player'), $score - option('score_per_player'),
User::find($user->uid)->score User::find($user->uid)->score
); );
// Single player
option(['single_player' => true]);
$this->postJson(route('user.player.add'), ['name' => 'abc'])
->assertJson([
'code' => 1,
'message' => trans('user.player.add.single'),
]);
} }
public function testDelete() public function testDelete()
@ -234,17 +226,6 @@ class PlayerControllerTest extends TestCase
$user->score, $user->score,
User::find($user->uid)->score User::find($user->uid)->score
); );
// Single player
option(['single_player' => true]);
$player = factory(Player::class)->create(['uid' => $user->uid]);
$this->actingAs($user)
->deleteJson(route('user.player.delete', ['player' => $player]))
->assertJson([
'code' => 1,
'message' => trans('user.player.delete.single'),
]);
$this->assertNotNull(Player::find($player->pid));
} }
public function testRename() public function testRename()
@ -329,14 +310,6 @@ class PlayerControllerTest extends TestCase
return true; return true;
}); });
// Single player
option(['single_player' => true]);
$this->putJson(
route('user.player.rename', ['player' => $player]),
['name' => 'abc']
)->assertJson(['code' => 0]);
$this->assertEquals('abc', $player->user->nickname);
} }
public function testSetTexture() public function testSetTexture()
@ -485,53 +458,4 @@ class PlayerControllerTest extends TestCase
return true; return true;
}); });
} }
public function testBind()
{
Event::fake();
option(['single_player' => true]);
$user = factory(User::class)->create();
$this->actingAs($user)
->postJson('/user/player/bind')
->assertJsonValidationErrors('player');
$this->postJson('/user/player/bind', ['player' => 'abc'])
->assertJson([
'code' => 0,
'message' => trans('user.player.bind.success'),
]);
Event::assertDispatched('player.adding', function ($event, $payload) use ($user) {
$this->assertEquals('abc', $payload[0]);
$this->assertEquals($user->uid, $payload[1]->uid);
return true;
});
Event::assertDispatched('player.added', function ($event, $payload) use ($user) {
$this->assertEquals('abc', $payload[0]->name);
$this->assertEquals($user->uid, $payload[1]->uid);
return true;
});
Event::assertDispatched(Events\PlayerWillBeAdded::class);
Event::assertDispatched(Events\PlayerWasAdded::class);
$player = Player::where('name', 'abc')->first();
$this->assertNotNull($player);
$this->assertEquals($user->uid, $player->uid);
$this->assertEquals('abc', $player->name);
$user->refresh();
$this->assertEquals('abc', $user->nickname);
$player2 = factory(Player::class)->create();
$player3 = factory(Player::class)->create(['uid' => $user->uid]);
$this->postJson('/user/player/bind', ['player' => $player2->name])
->assertJson([
'code' => 1,
'message' => trans('user.player.rename.repeated'),
]);
$this->postJson('/user/player/bind', ['player' => $player->name])
->assertJson(['code' => 0]);
$this->assertNull(Player::where('name', $player3->name)->first());
}
} }

View File

@ -76,16 +76,6 @@ class PlayersManagementControllerTest extends TestCase
['player_name' => $player->name] ['player_name' => $player->name]
)->assertJsonValidationErrors(['player_name']); )->assertJsonValidationErrors(['player_name']);
// single player
option(['single_player' => true]);
$this->putJson(
route('admin.players.name', ['player' => $player->pid]),
['player_name' => 'abc']
)->assertJson(['code' => 0]);
$player->refresh();
$this->assertEquals('abc', $player->user->nickname);
option(['single_player' => false]);
// rename a player successfully // rename a player successfully
Event::fake(); Event::fake();
$this->putJson( $this->putJson(
@ -99,7 +89,7 @@ class PlayersManagementControllerTest extends TestCase
$player->refresh(); $player->refresh();
$this->assertEquals('new_name', $player->name); $this->assertEquals('new_name', $player->name);
Event::assertDispatched( Event::assertDispatched(
'player.name.updating', 'player.renaming',
function ($eventName, $payload) use ($player) { function ($eventName, $payload) use ($player) {
$this->assertEquals($player->pid, $payload[0]->pid); $this->assertEquals($player->pid, $payload[0]->pid);
$this->assertEquals('new_name', $payload[1]); $this->assertEquals('new_name', $payload[1]);
@ -108,7 +98,7 @@ class PlayersManagementControllerTest extends TestCase
} }
); );
Event::assertDispatched( Event::assertDispatched(
'player.name.updated', 'player.renamed',
function ($eventName, $payload) use ($player, $oldName) { function ($eventName, $payload) use ($player, $oldName) {
$this->assertEquals($player->pid, $payload[0]->pid); $this->assertEquals($player->pid, $payload[0]->pid);
$this->assertEquals($oldName, $payload[1]); $this->assertEquals($oldName, $payload[1]);

View File

@ -267,13 +267,6 @@ class UserControllerTest extends TestCase
$this->postJson('/user/profile', ['action' => 'nickname']) $this->postJson('/user/profile', ['action' => 'nickname'])
->assertJsonValidationErrors('new_nickname'); ->assertJsonValidationErrors('new_nickname');
// Single player
option(['single_player' => true]);
factory(\App\Models\Player::class)->create(['uid' => $user->uid]);
$this->postJson('/user/profile', ['action' => 'nickname'])
->assertJson(['code' => 1, 'message' => trans('user.profile.nickname.single')]);
option(['single_player' => false]);
// Change nickname successfully // Change nickname successfully
$this->postJson('/user/profile', [ $this->postJson('/user/profile', [
'action' => 'nickname', 'action' => 'nickname',

View File

@ -1,30 +0,0 @@
<?php
namespace Tests;
use App\Models\Player;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class RequireBindPlayerTest extends TestCase
{
use DatabaseTransactions;
public function testHandle()
{
$user = factory(User::class)->create();
$this->actingAs($user)->get('/user')->assertViewIs('user.index');
$this->get('/user/player/bind')->assertRedirect('/user');
option(['single_player' => true]);
$this->getJson('/user/player/list')->assertHeader('content-type', 'application/json');
$this->get('/user/player/bind')->assertViewIs('user.bind');
$this->get('/user')->assertRedirect('/user/player/bind');
factory(Player::class)->create(['uid' => $user->uid]);
$this->get('/user')->assertViewIs('user.index');
$this->get('/user/player/bind')->assertRedirect('/user');
}
}