mirror of
https://github.com/crivion/laranode.git
synced 2026-09-03 06:24:09 +08:00
Create & Delete Accounts - only php fpm pools remaining
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Accounts;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
|
||||
class CreateAccountAction
|
||||
{
|
||||
public function execute(array $validated): void
|
||||
{
|
||||
$user = User::create($validated);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
if (isset($validated['notify']) && $validated['notify']) {
|
||||
\Illuminate\Support\Facades\Log::info('Would notify ' . $user->email);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ class RenameFileAction
|
||||
|
||||
// get path from currentName
|
||||
$path = dirname($r->currentName) == "." ? '' : dirname($r->currentName) . '/';
|
||||
$newPath = $path . $r->newName;
|
||||
|
||||
if ($filesystem->fileExists($newPath) || $filesystem->directoryExists($newPath)) {
|
||||
throw new \Exception('Target ' . $newPath . ' already exists!');
|
||||
}
|
||||
|
||||
$filesystem->move($r->currentName, $path . $r->newName);
|
||||
|
||||
@@ -39,3 +44,4 @@ class RenameFileAction
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Services\SystemStatsService;
|
||||
use App\Services\Dashboard\SystemStatsService;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Services\TopCommandService;
|
||||
use App\Services\Dashboard\TopCommandService;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBeUnique;
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Accounts\CreateAccountAction;
|
||||
use App\Http\Requests\CreateAccountRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Accounts\CreateAccountException;
|
||||
use App\Services\Accounts\CreateAccountService;
|
||||
use App\Services\Accounts\DeleteAccountService;
|
||||
use Exception;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -13,7 +17,7 @@ class AccountsController extends Controller
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
public function index(): \Inertia\Response
|
||||
{
|
||||
$accounts = User::all();
|
||||
return Inertia::render('Accounts/Index', compact('accounts'));
|
||||
@@ -22,11 +26,22 @@ class AccountsController extends Controller
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(CreateAccountRequest $request, CreateAccountAction $createAccount)
|
||||
public function store(CreateAccountRequest $request): RedirectResponse
|
||||
{
|
||||
$createAccount->execute($request->validated());
|
||||
try {
|
||||
|
||||
return redirect()->route('accounts.index');
|
||||
(new CreateAccountService($request->validated()))->handle();
|
||||
|
||||
session()->flash('success', 'Account created successfully!');
|
||||
|
||||
return redirect()->route('accounts.index');
|
||||
} catch (CreateAccountException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
return back();
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +56,9 @@ class AccountsController extends Controller
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy($account)
|
||||
public function destroy($account): RedirectResponse
|
||||
{
|
||||
User::findOrFail($account)->delete();
|
||||
(new DeleteAccountService(User::findOrFail($account)))->handle();
|
||||
|
||||
return redirect()->route('accounts.index');
|
||||
}
|
||||
@@ -51,7 +66,7 @@ class AccountsController extends Controller
|
||||
/**
|
||||
* Impersonate a user
|
||||
*/
|
||||
public function impersonate(User $user)
|
||||
public function impersonate(User $user): RedirectResponse
|
||||
{
|
||||
auth()->user()->impersonate($user);
|
||||
return redirect()->route('dashboard');
|
||||
@@ -60,7 +75,7 @@ class AccountsController extends Controller
|
||||
/**
|
||||
* Leave impersonation
|
||||
*/
|
||||
public function leaveImpersonation()
|
||||
public function leaveImpersonation(): RedirectResponse
|
||||
{
|
||||
auth()->user()->leaveImpersonation();
|
||||
return redirect()->route('dashboard');
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\CPUHistoryService;
|
||||
use App\Services\MemoryHistoryService;
|
||||
use App\Services\NetworkHistoryService;
|
||||
use App\Services\Dashboard\CPUHistoryService;
|
||||
use App\Services\Dashboard\MemoryHistoryService;
|
||||
use App\Services\Dashboard\NetworkHistoryService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -35,6 +35,10 @@ class HandleInertiaRequests extends Middleware
|
||||
'user' => $request->user(),
|
||||
'isImpersonating' => app('impersonate')->isImpersonating(),
|
||||
],
|
||||
'flash' => [
|
||||
'success' => session('success'),
|
||||
'error' => session('error'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
@@ -49,6 +51,7 @@ class User extends Authenticatable
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'ssh_access' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -60,8 +63,28 @@ class User extends Authenticatable
|
||||
return $this->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Attribute
|
||||
*/
|
||||
public function homedir(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => '/home/' . $this->systemUsername,
|
||||
);
|
||||
}
|
||||
|
||||
public function systemUsername(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->username . '_ln',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
app/Services/Accounts/CreateAccountService.php
Normal file
62
app/Services/Accounts/CreateAccountService.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Accounts;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Exception;
|
||||
|
||||
class CreateAccountException extends Exception {}
|
||||
|
||||
class CreateAccountService
|
||||
{
|
||||
private string $laranodeBinPath;
|
||||
private string $systemUsername;
|
||||
|
||||
public function __construct(private array $validated)
|
||||
{
|
||||
// path to laranode user manager bin|ssh script
|
||||
$this->laranodeBinPath = '/usr/local/bin/laranode';
|
||||
|
||||
// appends _ln to all users to avoid all sort of issues (conflicts, control, security, files, etc.)
|
||||
$this->systemUsername = $validated['username'] . '_ln';
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// create system user
|
||||
$this->createSystemUser();
|
||||
|
||||
// only after that add the user to the database
|
||||
$user = User::create($this->validated);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
// notify user if requested
|
||||
// @todo: implement notification (mail)
|
||||
if (isset($validated['notify']) && $validated['notify']) {
|
||||
\Illuminate\Support\Facades\Log::info('Would notify ' . $user->email);
|
||||
}
|
||||
}
|
||||
|
||||
private function createSystemUser(): void
|
||||
{
|
||||
|
||||
$createUser = Process::run([
|
||||
'sudo',
|
||||
$this->laranodeBinPath . '/laranode-user-manager.sh',
|
||||
'create',
|
||||
$this->systemUsername,
|
||||
$this->validated['ssh_access'] ? 'yes' : 'no',
|
||||
$this->validated['ssh_access'] ? $this->validated['password'] : null,
|
||||
]);
|
||||
|
||||
if ($createUser->failed()) {
|
||||
throw new CreateAccountException('Failed to create system user: ' . $createUser->errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: implement add user php-fpm pools based on each php version
|
||||
private function addPhpFpmPools(): void {}
|
||||
}
|
||||
47
app/Services/Accounts/DeleteAccountService.php
Normal file
47
app/Services/Accounts/DeleteAccountService.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Accounts;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Exception;
|
||||
|
||||
class DeleteAccountException extends Exception {}
|
||||
|
||||
class DeleteAccountService
|
||||
{
|
||||
private string $laranodeBinPath;
|
||||
|
||||
public function __construct(private User $user)
|
||||
{
|
||||
$this->laranodeBinPath = '/usr/local/bin/laranode';
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// delete system user
|
||||
$this->deleteSystemUser();
|
||||
|
||||
User::findOrFail($this->user->id)->delete();
|
||||
}
|
||||
|
||||
private function deleteSystemUser(): void
|
||||
{
|
||||
$deleteUser = Process::run([
|
||||
'sudo',
|
||||
$this->laranodeBinPath . '/laranode-user-manager.sh',
|
||||
'delete',
|
||||
$this->user->systemUsername,
|
||||
]);
|
||||
|
||||
if ($deleteUser->failed()) {
|
||||
throw new DeleteAccountException('Failed to delete system user: ' . $deleteUser->errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: implement delete all DB's of this user
|
||||
private function deleteDatabases(): void {}
|
||||
|
||||
// @TODO: implement delete user php-fpm pools
|
||||
private function deletePhpFpmPools(): void {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\Dashboard;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\Dashboard;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\Dashboard;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\Dashboard;
|
||||
|
||||
use App\Services\Contracts\HistoricStatsContract;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\Dashboard;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\Dashboard;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
@@ -1,13 +1,25 @@
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import TopNavi from './Partials/TopNavi';
|
||||
import SidebarNavi from './Partials/SidebarNavi';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
|
||||
export default function AuthenticatedLayout({ header, children }) {
|
||||
const user = usePage().props.auth.user;
|
||||
const { flash } = usePage().props;
|
||||
const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (flash.success) {
|
||||
toast(flash.success, { type: 'success' });
|
||||
}
|
||||
|
||||
if (flash.error) {
|
||||
toast(flash.error, { type: 'error' });
|
||||
}
|
||||
}, [flash]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col flex-auto flex-shrink-0 antialiase bg-gray-100 dark:bg-gray-900">
|
||||
<ToastContainer theme='dark' />
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function Accounts({ accounts }) {
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{account.ssh_access ? <span className='bg-amber-200 text-amber-700 px-2 py-1 rounded-lg'>No</span> : <span className='bg-lime-300 text-lime-700 px-2 py-0.5 text-sm rounded-lg'>Yes</span>}
|
||||
{!account.ssh_access ? <span className='bg-amber-200 text-amber-700 px-2 py-1 rounded-lg'>No</span> : <span className='bg-lime-300 text-lime-700 px-2 py-0.5 text-sm rounded-lg'>Yes</span>}
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{account.role == "admin" ? <span className='bg-green-300 text-green-700 px-2 py-1 text-sm rounded-lg'>Admin</span> : <span className='bg-gray-300 text-gray-700 px-2 py-1 text-sm rounded-lg'>User</span>}
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function CreateAccountForm() {
|
||||
closeModal();
|
||||
reset();
|
||||
toast("Account created successfully.");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
108
tests/Feature/Filemanager/DeleteFileTest.php
Normal file
108
tests/Feature/Filemanager/DeleteFileTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Filemanager\DeleteFilesAction;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->filesystem = new Filesystem(new InMemoryFilesystemAdapter());
|
||||
$this->action = new DeleteFilesAction($this->filesystem);
|
||||
|
||||
// Set up test directory structure
|
||||
$this->filesystem->write('file1.txt', 'content');
|
||||
$this->filesystem->write('file2.txt', 'content');
|
||||
$this->filesystem->createDirectory('folder1');
|
||||
$this->filesystem->write('folder1/inside.txt', 'content');
|
||||
$this->filesystem->createDirectory('folder2');
|
||||
$this->filesystem->write('folder2/test.txt', 'content');
|
||||
});
|
||||
|
||||
test('it can delete a single file', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => ['file1.txt']
|
||||
]);
|
||||
|
||||
$response = $this->action->execute($request);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($this->filesystem->fileExists('file1.txt'))->toBeFalse()
|
||||
->and(json_decode($response->getContent()))->toHaveProperty('message', 'Files deleted successfully!');
|
||||
});
|
||||
|
||||
test('it can delete multiple files', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => ['file1.txt', 'file2.txt']
|
||||
]);
|
||||
|
||||
$response = $this->action->execute($request);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($this->filesystem->fileExists('file1.txt'))->toBeFalse()
|
||||
->and($this->filesystem->fileExists('file2.txt'))->toBeFalse()
|
||||
->and(json_decode($response->getContent()))->toHaveProperty('message', 'Files deleted successfully!');
|
||||
});
|
||||
|
||||
test('it can delete a directory and its contents', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => ['folder1']
|
||||
]);
|
||||
|
||||
$response = $this->action->execute($request);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($this->filesystem->directoryExists('folder1'))->toBeFalse()
|
||||
->and($this->filesystem->fileExists('folder1/inside.txt'))->toBeFalse()
|
||||
->and(json_decode($response->getContent()))->toHaveProperty('message', 'Files deleted successfully!');
|
||||
});
|
||||
|
||||
test('it can delete multiple directories and files together', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => ['folder1', 'file1.txt', 'folder2']
|
||||
]);
|
||||
|
||||
$response = $this->action->execute($request);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($this->filesystem->directoryExists('folder1'))->toBeFalse()
|
||||
->and($this->filesystem->fileExists('file1.txt'))->toBeFalse()
|
||||
->and($this->filesystem->directoryExists('folder2'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('it fails when file does not exist', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => ['non-existent.txt']
|
||||
]);
|
||||
|
||||
$response = $this->action->execute($request);
|
||||
|
||||
expect($response->getStatusCode())->toBe(500)
|
||||
->and(json_decode($response->getContent()))->toHaveProperty('error', 'non-existent.txt does not exist!');
|
||||
});
|
||||
|
||||
test('it fails when directory does not exist', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => ['non-existent-folder']
|
||||
]);
|
||||
|
||||
$response = $this->action->execute($request);
|
||||
|
||||
expect($response->getStatusCode())->toBe(500)
|
||||
->and(json_decode($response->getContent()))->toHaveProperty('error', 'non-existent-folder does not exist!');
|
||||
});
|
||||
|
||||
test('it fails validation when filesToDelete is not provided', function () {
|
||||
$request = Request::create('', 'POST', []);
|
||||
|
||||
expect(fn() => $this->action->execute($request))
|
||||
->toThrow(Illuminate\Validation\ValidationException::class);
|
||||
});
|
||||
|
||||
test('it fails validation when filesToDelete is not an array', function () {
|
||||
$request = Request::create('', 'POST', [
|
||||
'filesToDelete' => 'not-an-array'
|
||||
]);
|
||||
|
||||
expect(fn() => $this->action->execute($request))
|
||||
->toThrow(Illuminate\Validation\ValidationException::class);
|
||||
});
|
||||
@@ -3,12 +3,12 @@
|
||||
use App\Actions\Filemanager\GetDirectoryContentsAction;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->filesystem = new Filesystem(new InMemoryFilesystemAdapter());
|
||||
$this->action = new GetDirectoryContentsAction($this->filesystem);
|
||||
|
||||
// Set up a test directory structure
|
||||
$this->filesystem->write('file1.txt', 'content');
|
||||
$this->filesystem->write('file2.txt', 'content');
|
||||
$this->filesystem->createDirectory('folder1');
|
||||
@@ -17,53 +17,59 @@ beforeEach(function () {
|
||||
$this->filesystem->write('folder1/subfolder/deep.txt', 'content');
|
||||
});
|
||||
|
||||
// Helper function to capture streamed response content
|
||||
function captureStreamedContent($response): array
|
||||
{
|
||||
if ($response instanceof StreamedResponse) {
|
||||
ob_start();
|
||||
$response->sendContent();
|
||||
$content = ob_get_clean();
|
||||
return json_decode($content, true);
|
||||
}
|
||||
return json_decode($response->getContent(), true);
|
||||
}
|
||||
|
||||
test('it lists contents of root directory', function () {
|
||||
$response = $this->action->execute(null);
|
||||
$content = json_decode($response->getContent(), true);
|
||||
$response = $this->action->execute('/');
|
||||
$content = captureStreamedContent($response);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($content)->toHaveKey('files')
|
||||
->and($content)->toHaveKey('goBack')
|
||||
->and($content['goBack'])->toBe('/')
|
||||
->and(collect($content['files'])->pluck('path')->all())->toContain('file1.txt')
|
||||
->and(collect($content['files'])->pluck('path')->all())->toContain('file2.txt')
|
||||
->and(collect($content['files'])->pluck('path')->all())->toContain('folder1');
|
||||
->and($content['goBack'])->toBe('')
|
||||
->and(collect($content['files'])->map(fn($file) => $file['path'])->all())->toContain('file1.txt')
|
||||
->and(collect($content['files'])->map(fn($file) => $file['path'])->all())->toContain('file2.txt')
|
||||
->and(collect($content['files'])->map(fn($file) => $file['path'])->all())->toContain('folder1');
|
||||
});
|
||||
|
||||
test('it lists contents of a subdirectory', function () {
|
||||
$response = $this->action->execute('folder1');
|
||||
$content = json_decode($response->getContent(), true);
|
||||
$content = captureStreamedContent($response);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($content)->toHaveKey('files')
|
||||
->and($content)->toHaveKey('goBack')
|
||||
->and($content['goBack'])->toBe('/')
|
||||
->and(collect($content['files'])->pluck('path')->all())->toContain('folder1/inside1.txt')
|
||||
->and(collect($content['files'])->pluck('path')->all())->toContain('folder1/subfolder');
|
||||
->and(collect($content['files'])->map(fn($file) => $file['path'])->all())->toContain('folder1/inside1.txt')
|
||||
->and(collect($content['files'])->map(fn($file) => $file['path'])->all())->toContain('folder1/subfolder');
|
||||
});
|
||||
|
||||
test('it handles nested directory navigation', function () {
|
||||
$response = $this->action->execute('folder1/subfolder');
|
||||
$content = json_decode($response->getContent(), true);
|
||||
$content = captureStreamedContent($response);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($content)->toHaveKey('files')
|
||||
->and($content)->toHaveKey('goBack')
|
||||
->and($content['goBack'])->toBe('folder1')
|
||||
->and(collect($content['files'])->pluck('path')->all())->toContain('folder1/subfolder/deep.txt');
|
||||
->and(collect($content['files'])->map(fn($file) => $file['path'])->all())->toContain('folder1/subfolder/deep.txt');
|
||||
});
|
||||
|
||||
test('it handles non-existent directory', function () {
|
||||
$response = $this->action->execute('non-existent-folder');
|
||||
|
||||
expect($response->getStatusCode())->toBe(500)
|
||||
->and(json_decode($response->getContent(), true))->toHaveKey('error');
|
||||
});
|
||||
|
||||
test('it handles empty directory', function () {
|
||||
$this->filesystem->createDirectory('empty-folder');
|
||||
$response = $this->action->execute('empty-folder');
|
||||
$content = json_decode($response->getContent(), true);
|
||||
$content = captureStreamedContent($response);
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($content)->toHaveKey('files')
|
||||
@@ -72,9 +78,9 @@ test('it handles empty directory', function () {
|
||||
|
||||
test('it returns non-recursive listing', function () {
|
||||
$response = $this->action->execute('folder1');
|
||||
$content = json_decode($response->getContent(), true);
|
||||
$content = captureStreamedContent($response);
|
||||
|
||||
expect(collect($content['files'])->pluck('path')->all())
|
||||
expect(collect($content['files'])->map(fn($file) => $file['path'])->all())
|
||||
->not->toContain('folder1/subfolder/deep.txt');
|
||||
});
|
||||
|
||||
@@ -82,6 +88,7 @@ test('it properly handles directory traversal attempts', function () {
|
||||
$response = $this->action->execute('../some/path');
|
||||
$content = json_decode($response->getContent(), true);
|
||||
|
||||
// The action should treat this as a regular path and fail to find it
|
||||
expect($response->getStatusCode())->toBe(500)
|
||||
->and($content)->toHaveKey('error');
|
||||
});
|
||||
|
||||
40
tests/Feature/Filemanager/RenameFileTest.php
Normal file
40
tests/Feature/Filemanager/RenameFileTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Filemanager\RenameFileAction;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
|
||||
|
||||
test('it can rename a directory', function () {
|
||||
$filesystem = new Filesystem(new InMemoryFilesystemAdapter());
|
||||
$action = new RenameFileAction($filesystem);
|
||||
|
||||
// Create a directory with a file inside to test full directory move
|
||||
$filesystem->createDirectory('test-folder');
|
||||
$filesystem->write('test-folder/inside.txt', 'test content');
|
||||
|
||||
$request = Request::create('', 'POST', [
|
||||
'currentName' => 'test-folder',
|
||||
'newName' => 'renamed-folder'
|
||||
]);
|
||||
|
||||
// Let's see what's happening
|
||||
try {
|
||||
$response = $action->execute($request);
|
||||
|
||||
dump($response->getStatusCode());
|
||||
|
||||
// Dump response content if there's an error
|
||||
if ($response->getStatusCode() === 500) {
|
||||
dump(json_decode($response->getContent(), true));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
dump($e->getMessage());
|
||||
}
|
||||
|
||||
expect($response->getStatusCode())->toBe(200)
|
||||
->and($filesystem->directoryExists('test-folder'))->toBeFalse()
|
||||
->and($filesystem->directoryExists('renamed-folder'))->toBeTrue()
|
||||
->and($filesystem->fileExists('renamed-folder/inside.txt'))->toBeTrue();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Services\TopCommandService;
|
||||
use App\Services\Dashboard\TopCommandService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user