add closet command for Web CLI
This commit is contained in:
parent
d57dabe188
commit
4c981529e1
34
app/Http/Controllers/ClosetManagementController.php
Normal file
34
app/Http/Controllers/ClosetManagementController.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClosetManagementController extends Controller
|
||||
{
|
||||
public function add(Request $request, $uid)
|
||||
{
|
||||
/** @var Texture */
|
||||
$texture = Texture::findOrFail($request->input('tid'));
|
||||
|
||||
/** @var User */
|
||||
$user = User::findOrFail($uid);
|
||||
$user->closet()->attach($texture->tid, ['item_name' => $texture->name]);
|
||||
|
||||
return json('', 0, compact('user', 'texture'));
|
||||
}
|
||||
|
||||
public function remove(Request $request, $uid)
|
||||
{
|
||||
/** @var Texture */
|
||||
$texture = Texture::findOrFail($request->input('tid'));
|
||||
|
||||
/** @var User */
|
||||
$user = User::findOrFail($uid);
|
||||
$user->closet()->detach($texture->tid);
|
||||
|
||||
return json('', 0, compact('user', 'texture'));
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@
|
||||
"@hot-loader/react-dom": "^16.11.0",
|
||||
"@tweenjs/tween.js": "^18.4.2",
|
||||
"admin-lte": "^3.0.1",
|
||||
"blessing-skin-shell": "^0.1.0",
|
||||
"blessing-skin-shell": "^0.2.0",
|
||||
"echarts": "^4.6.0",
|
||||
"jquery": "^3.4.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@ -224,7 +224,8 @@
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"<rootDir>/resources/assets/tests/setup",
|
||||
"<rootDir>/resources/assets/tests/utils"
|
||||
"<rootDir>/resources/assets/tests/utils",
|
||||
"<rootDir>/resources/assets/tests/scripts/cli/stdio"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/resources/assets/tests/**/*.test.ts",
|
||||
|
@ -5,6 +5,7 @@ import { FitAddon } from 'xterm-addon-fit'
|
||||
import { Shell } from 'blessing-skin-shell'
|
||||
import 'xterm/css/xterm.css'
|
||||
import Draggable from 'react-draggable'
|
||||
import ClosetCommand from './cli/ClosetCommand'
|
||||
import styles from '@/styles/terminal.module.scss'
|
||||
|
||||
let launched = false
|
||||
@ -29,6 +30,8 @@ const TerminalWindow: React.FC<{ onClose(): void }> = props => {
|
||||
fitAddon.fit()
|
||||
|
||||
const shell = new Shell(terminal)
|
||||
shell.addExternal('closet', ClosetCommand)
|
||||
|
||||
const unbind = terminal.onData(e => shell.input(e))
|
||||
launched = true
|
||||
|
||||
|
63
resources/assets/src/scripts/cli/ClosetCommand.ts
Normal file
63
resources/assets/src/scripts/cli/ClosetCommand.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type { Stdio } from 'blessing-skin-shell'
|
||||
import * as fetch from '../net'
|
||||
import { User, Texture } from '../types'
|
||||
|
||||
type Subcommand = 'add' | 'remove'
|
||||
|
||||
type Response = fetch.ResponseBody<{ user: User; texture: Texture }>
|
||||
|
||||
export default async function closet(stdio: Stdio, args: string[]) {
|
||||
if (args.includes('-h') || args.includes('--help')) {
|
||||
stdio.println('Usage: closet <add|remove> <uid> <tid>')
|
||||
return
|
||||
}
|
||||
|
||||
const command = args[0] as Subcommand
|
||||
const uid = args[1]
|
||||
const tid = args[2]
|
||||
|
||||
if (!command) {
|
||||
stdio.println('Supported subcommand: add, remove.')
|
||||
return
|
||||
}
|
||||
if (!uid) {
|
||||
stdio.println('User ID must be provided.')
|
||||
return
|
||||
}
|
||||
if (!tid) {
|
||||
stdio.println('Texture ID must be provided.')
|
||||
return
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'add': {
|
||||
const { code, data } = await fetch.post<Response>(
|
||||
`/admin/closet/${uid}`,
|
||||
{ tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
const { texture, user } = data
|
||||
stdio.println(
|
||||
`Texture "${texture.name}" was added to user ${user.nickname}'s closet.`,
|
||||
)
|
||||
} else {
|
||||
stdio.println('Error occurred.')
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'remove': {
|
||||
const { code, data } = await fetch.del<Response>(`/admin/closet/${uid}`, {
|
||||
tid,
|
||||
})
|
||||
if (code === 0) {
|
||||
const { texture, user } = data
|
||||
stdio.println(
|
||||
`Texture "${texture.name}" was removed from user ${user.nickname}'s closet.`,
|
||||
)
|
||||
} else {
|
||||
stdio.println('Error occurred.')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,16 @@
|
||||
export type User = {
|
||||
uid: number
|
||||
email: string
|
||||
nickname: string
|
||||
score: number
|
||||
avatar: number
|
||||
permission: number
|
||||
ip: string
|
||||
last_sign_at: string
|
||||
register_at: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export type Player = {
|
||||
pid: number
|
||||
name: string
|
||||
|
@ -1,7 +1,7 @@
|
||||
@use '../styles/breakpoints';
|
||||
|
||||
.terminal {
|
||||
z-index: 1060;
|
||||
z-index: 1040;
|
||||
position: fixed;
|
||||
bottom: 7vh;
|
||||
user-select: none;
|
||||
|
101
resources/assets/tests/scripts/cli/ClosetCommand.test.ts
Normal file
101
resources/assets/tests/scripts/cli/ClosetCommand.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import * as fetch from '@/scripts/net'
|
||||
import runCommand from '@/scripts/cli/ClosetCommand'
|
||||
import { Stdio } from './stdio'
|
||||
|
||||
jest.mock('@/scripts/net')
|
||||
|
||||
test('help message', async () => {
|
||||
let stdio = new Stdio()
|
||||
await runCommand(stdio, ['-h'])
|
||||
expect(stdio.getStdout()).toInclude('Usage')
|
||||
|
||||
stdio = new Stdio()
|
||||
await runCommand(stdio, ['--help'])
|
||||
expect(stdio.getStdout()).toInclude('Usage')
|
||||
})
|
||||
|
||||
test('missing subcommand', async () => {
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, [])
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('unsupported subcommand', async () => {
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['abc'])
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('missing uid', async () => {
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['add'])
|
||||
expect(stdio.getStdout()).toInclude('User ID')
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('missing tid', async () => {
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['add', '1'])
|
||||
expect(stdio.getStdout()).toInclude('Texture ID')
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
})
|
||||
|
||||
describe('add texture', () => {
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({
|
||||
code: 0,
|
||||
data: { user: { nickname: 'kumiko' }, texture: { name: 'eupho' } },
|
||||
})
|
||||
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['add', '1', '2'])
|
||||
|
||||
const stdout = stdio.getStdout()
|
||||
expect(stdout).toInclude('kumiko')
|
||||
expect(stdout).toInclude('eupho')
|
||||
expect(fetch.post).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1 })
|
||||
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['add', '1', '2'])
|
||||
|
||||
const stdout = stdio.getStdout()
|
||||
expect(stdout).toInclude('Error occurred.')
|
||||
expect(fetch.post).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove texture', () => {
|
||||
it('succeeded', async () => {
|
||||
fetch.del.mockResolvedValue({
|
||||
code: 0,
|
||||
data: { user: { nickname: 'kumiko' }, texture: { name: 'eupho' } },
|
||||
})
|
||||
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['remove', '1', '2'])
|
||||
|
||||
const stdout = stdio.getStdout()
|
||||
expect(stdout).toInclude('kumiko')
|
||||
expect(stdout).toInclude('eupho')
|
||||
expect(fetch.del).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.del.mockResolvedValue({ code: 1 })
|
||||
|
||||
const stdio = new Stdio()
|
||||
await runCommand(stdio, ['remove', '1', '2'])
|
||||
|
||||
const stdout = stdio.getStdout()
|
||||
expect(stdout).toInclude('Error occurred.')
|
||||
expect(fetch.del).toBeCalledWith('/admin/closet/1', { tid: '2' })
|
||||
})
|
||||
})
|
24
resources/assets/tests/scripts/cli/stdio.ts
Normal file
24
resources/assets/tests/scripts/cli/stdio.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export class Stdio {
|
||||
private stdout = ''
|
||||
|
||||
public print(data: string) {
|
||||
this.stdout += data
|
||||
}
|
||||
|
||||
public println(data: string) {
|
||||
this.stdout += data
|
||||
this.stdout += '\r\n'
|
||||
}
|
||||
|
||||
public reset() {
|
||||
//
|
||||
}
|
||||
|
||||
public free() {
|
||||
//
|
||||
}
|
||||
|
||||
public getStdout() {
|
||||
return this.stdout
|
||||
}
|
||||
}
|
@ -140,6 +140,11 @@ Route::prefix('admin')
|
||||
Route::get('list', 'AdminController@getPlayerData');
|
||||
});
|
||||
|
||||
Route::prefix('closet')->group(function () {
|
||||
Route::post('{uid}', 'ClosetManagementController@add');
|
||||
Route::delete('{uid}', 'ClosetManagementController@remove');
|
||||
});
|
||||
|
||||
Route::prefix('reports')->group(function () {
|
||||
Route::view('', 'admin.reports');
|
||||
Route::post('', 'ReportController@review');
|
||||
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
class ClosetManagementControllerTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(factory(\App\Models\User::class)->states('admin')->create());
|
||||
}
|
||||
|
||||
public function testAdd()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
$texture = factory(Texture::class)->create();
|
||||
|
||||
$this->postJson('/admin/closet/'.$user->uid, ['tid' => $texture->tid])
|
||||
->assertJson([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'user' => $user->toArray(),
|
||||
'texture' => $texture->toArray(),
|
||||
],
|
||||
]);
|
||||
$item = $user->closet()->first();
|
||||
$this->assertEquals($texture->tid, $item->tid);
|
||||
$this->assertEquals($texture->name, $item->pivot->item_name);
|
||||
}
|
||||
|
||||
public function testRemove()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
$texture = factory(Texture::class)->create();
|
||||
$user->closet()->attach($texture->tid, ['item_name' => '']);
|
||||
|
||||
$this->deleteJson('/admin/closet/'.$user->uid, ['tid' => $texture->tid])
|
||||
->assertJson([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'user' => $user->toArray(),
|
||||
'texture' => $texture->toArray(),
|
||||
],
|
||||
]);
|
||||
$this->assertCount(0, $user->closet);
|
||||
}
|
||||
}
|
@ -2267,10 +2267,10 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
|
||||
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
||||
|
||||
blessing-skin-shell@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/blessing-skin-shell/-/blessing-skin-shell-0.1.0.tgz#6251b154d2e5b225d65f1b6e8c7489bb2eb67303"
|
||||
integrity sha512-6c7RwPqLS1mXsrLd6QU7xUjr/gfmDgAY5L+CqOMWwwjLfjuohw6SK6cnGyfWxeK8KoDj5zX3QyoYuCKapXlkUg==
|
||||
blessing-skin-shell@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/blessing-skin-shell/-/blessing-skin-shell-0.2.0.tgz#2454e305c69de134164491d2715a34d599c0d8a1"
|
||||
integrity sha512-bwVHWep3jlPwgY7rAmr+KZJr1RdfVS9OTQB25avRW4EzONflQRiRgU76T8X/DeWa3PItEEGf3qNUJJuRhN5orw==
|
||||
|
||||
bluebird@^3.1.1, bluebird@^3.5.5:
|
||||
version "3.5.5"
|
||||
|
Loading…
Reference in New Issue
Block a user