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\PlayerWillBeAdded;
use App\Events\PlayerWillBeDeleted;
use App\Http\Middleware\CheckPlayerOwner;
use App\Models\Player;
use App\Models\Texture;
use App\Models\User;
@ -77,10 +76,6 @@ class PlayerController extends Controller
/** @var User */
$user = Auth::user();
if (option('single_player', false)) {
return json(trans('user.player.add.single'), 1);
}
$name = $request->validate([
'name' => [
'required',
@ -138,10 +133,6 @@ class PlayerController extends Controller
return json($can->getReason(), 1);
}
if (option('single_player', false)) {
return json(trans('user.player.delete.single'), 1);
}
$dispatcher->dispatch('player.deleting', [$player, $user]);
event(new PlayerWillBeDeleted($player));
@ -186,13 +177,6 @@ class PlayerController extends Controller
$player->name = $name;
$player->save();
if (option('single_player', false)) {
/** @var User */
$user = auth()->user();
$user->nickname = $name;
$user->save();
}
$dispatcher->dispatch('player.renamed', [$player, $old]);
return json(
@ -262,41 +246,4 @@ class PlayerController extends Controller
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'];
$dispatcher->dispatch('player.name.updating', [$player, $name]);
$dispatcher->dispatch('player.renaming', [$player, $name]);
$oldName = $player->name;
$player->name = $name;
$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);
}

View File

@ -217,10 +217,6 @@ class UserController extends Controller
switch ($action) {
case 'nickname':
if (option('single_player', false)) {
return json(trans('user.profile.nickname.single'), 1);
}
$request->validate(['new_nickname' => 'required']);
$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' => [
Listeners\SerializeGlobals::class,
],
'player.name.updated' => [
Listeners\SinglePlayer\UpdateOwnerNickName::class,
],
'auth.registration.completed' => [
Listeners\SendEmailVerification::class,
],

View File

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

View File

@ -37,8 +37,8 @@ export default {
user: {
home: () => '/user' as const,
closet: {
add: () => '/user/closet' as const,
page: () => '/user/closet' as const,
add: () => '/user/closet' as const,
ids: () => '/user/closet/ids' as const,
list: () => '/user/closet/list' as const,
rename: (tid: number) => `/user/closet/${tid}`,
@ -48,7 +48,6 @@ export default {
player: {
add: () => '/user/player' as const,
page: () => '/user/player' as const,
bind: () => '/user/player/bind' as const,
list: () => '/user/player/list' as const,
delete: (player: number) => `/user/player/${player}`,
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
useAs: Apply...
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:
upload: Upload Texture
use-as:

View File

@ -87,11 +87,9 @@ player:
add:
repeated: The player name is already registered.
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.
delete:
single: You must own exactly ONE player so you can't delete it.
success: Player :name was deleted successfully.
rename:
@ -104,10 +102,6 @@ player:
clear:
success: The textures of player :name was resetted successfully.
bind:
title: Bind Players
success: Bound successfully!
profile:
avatar:
title: Change Avatar?
@ -128,7 +122,6 @@ profile:
nickname:
title: Change Nickname
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
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')
->name('user.')
->middleware(['authorize', Middleware\RequireBindPlayer::class])
->middleware(['authorize'])
->group(function () {
Route::get('', 'UserController@index')->name('home');
Route::get('notifications/{id}', 'NotificationsController@read')->name('notification');
@ -76,8 +76,6 @@ Route::prefix('user')
Route::delete('{player}/textures', 'PlayerController@clearTexture')->name('clear');
Route::put('{player}/name', 'PlayerController@rename')->name('rename');
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 () {

View File

@ -158,14 +158,6 @@ class PlayerControllerTest extends TestCase
$score - option('score_per_player'),
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()
@ -234,17 +226,6 @@ class PlayerControllerTest extends TestCase
$user->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()
@ -329,14 +310,6 @@ class PlayerControllerTest extends TestCase
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()
@ -485,53 +458,4 @@ class PlayerControllerTest extends TestCase
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]
)->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
Event::fake();
$this->putJson(
@ -99,7 +89,7 @@ class PlayersManagementControllerTest extends TestCase
$player->refresh();
$this->assertEquals('new_name', $player->name);
Event::assertDispatched(
'player.name.updating',
'player.renaming',
function ($eventName, $payload) use ($player) {
$this->assertEquals($player->pid, $payload[0]->pid);
$this->assertEquals('new_name', $payload[1]);
@ -108,7 +98,7 @@ class PlayersManagementControllerTest extends TestCase
}
);
Event::assertDispatched(
'player.name.updated',
'player.renamed',
function ($eventName, $payload) use ($player, $oldName) {
$this->assertEquals($player->pid, $payload[0]->pid);
$this->assertEquals($oldName, $payload[1]);

View File

@ -267,13 +267,6 @@ class UserControllerTest extends TestCase
$this->postJson('/user/profile', ['action' => '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
$this->postJson('/user/profile', [
'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');
}
}