From 477364d40bb7cf7b756de44a06986d657bc730ec Mon Sep 17 00:00:00 2001 From: DariusIII Date: Sun, 26 Apr 2026 11:38:57 +0200 Subject: [PATCH] Fix gzopen and user creation issues --- app/Extensions/helper/helpers.php | 2 +- .../Controllers/Admin/AdminUserController.php | 26 +- .../Feature/Admin/AdminUserControllerTest.php | 224 ++++++++++++++++++ tests/Unit/Extensions/HelperCoverUrlTest.php | 14 ++ 4 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/Admin/AdminUserControllerTest.php diff --git a/app/Extensions/helper/helpers.php b/app/Extensions/helper/helpers.php index ded022d00..ece174cd1 100644 --- a/app/Extensions/helper/helpers.php +++ b/app/Extensions/helper/helpers.php @@ -755,7 +755,7 @@ if (! function_exists('unzipGzipFile')) { function unzipGzipFile(string $filePath): false|string { $string = ''; - $gzFile = @gzopen($filePath, 'rb', 0); + $gzFile = @gzopen($filePath, 'rb'); if ($gzFile) { while (! gzeof($gzFile)) { $temp = gzread($gzFile, 1024); diff --git a/app/Http/Controllers/Admin/AdminUserController.php b/app/Http/Controllers/Admin/AdminUserController.php index 342690557..b36296881 100644 --- a/app/Http/Controllers/Admin/AdminUserController.php +++ b/app/Http/Controllers/Admin/AdminUserController.php @@ -108,10 +108,18 @@ class AdminUserController extends BasePageController // set the current action $action = $request->input('action') ?? 'view'; + $roleId = null; + if ($action === 'submit') { + $validated = $request->validate([ + 'role' => ['required', 'integer', 'exists:roles,id'], + ]); + $roleId = (int) $validated['role']; + } + // get the user roles $userRoles = Role::cursor()->remember(); $roles = []; - $defaultRole = 'User'; + $defaultRole = UserRole::USER->value; $defaultInvites = Invitation::DEFAULT_INVITES; foreach ($userRoles as $r) { $roles[$r->id] = $r->name; @@ -140,16 +148,16 @@ class AdminUserController extends BasePageController if (empty($request->input('id'))) { $invites = $defaultInvites; foreach ($userRoles as $role) { - if ($role['id'] === $request->input('role')) { + if ((int) $role['id'] === $roleId) { $invites = $role['defaultinvites']; } } - $ret = User::signUp($request->input('username'), $request->input('password'), $request->input('email'), '', $request->input('notes'), $invites, '', true, $request->input('role'), false); + $ret = User::signUp($request->input('username'), $request->input('password'), $request->input('email'), '', $request->input('notes'), $invites, '', true, $roleId, false); } else { $editedUser = User::find($request->input('id')); // Check if role is changing and get stack preference - $roleChanged = $editedUser->roles_id != $request->input('role'); + $roleChanged = (int) $editedUser->roles_id !== $roleId; $stackRole = $request->input('stack_role') ? true : false; // Check if checkbox is checked $changedBy = Auth::check() ? Auth::id() : null; @@ -164,7 +172,7 @@ class AdminUserController extends BasePageController 'user_id' => $editedUser->id, 'originalRoleChangeDate' => $originalRoleChangeDate, 'current_roles_id' => $editedUser->roles_id, - 'requested_role' => $request->input('role'), + 'requested_role' => $roleId, 'roleChanged' => $roleChanged, 'stackRole' => $stackRole, 'form_rolechangedate' => $request->input('rolechangedate'), @@ -198,17 +206,17 @@ class AdminUserController extends BasePageController // If role is changing, handle it with stacking logic // Pass the original expiry so history records the correct old_expiry_date - if ($roleChanged && $request->input('role') !== null) { + if ($roleChanged) { Log::info('AdminUserController - About to call updateUserRole', [ 'user_id' => $editedUser->id, - 'new_role' => (int) $request->input('role'), + 'new_role' => $roleId, 'originalRoleChangeDate_passed' => $originalRoleChangeDate, 'current_user_rolechangedate' => $editedUser->rolechangedate, ]); User::updateUserRole( $editedUser->id, - (int) $request->input('role'), // Cast to integer + $roleId, ! $adminManuallySetExpiry, // Only apply promotions if admin didn't set custom expiry $stackRole, // Stack role if requested $changedBy, @@ -259,7 +267,7 @@ class AdminUserController extends BasePageController 'id' => $request->input('id'), 'username' => $request->input('username'), 'email' => $request->input('email'), - 'role' => $request->input('role'), + 'role' => $roleId, 'notes' => $request->input('notes'), ]; break; diff --git a/tests/Feature/Admin/AdminUserControllerTest.php b/tests/Feature/Admin/AdminUserControllerTest.php new file mode 100644 index 000000000..76eda4249 --- /dev/null +++ b/tests/Feature/Admin/AdminUserControllerTest.php @@ -0,0 +1,224 @@ + 'sqlite', + 'database.connections.sqlite.database' => ':memory:', + 'app.key' => 'base64:'.base64_encode(random_bytes(32)), + 'session.driver' => 'array', + ]); + + DB::purge(); + DB::reconnect(); + + $this->createSchema(); + $this->seedSettings(); + app(PermissionRegistrar::class)->forgetCachedPermissions(); + $this->withoutMiddleware(Google2FAMiddleware::class); + } + + public function test_admin_can_create_user_with_string_role_id(): void + { + $admin = $this->createUserWithRole('Admin', false); + $userRole = Role::query()->firstOrCreate( + ['name' => 'User', 'guard_name' => 'web'], + ['rate_limit' => 60, 'isdefault' => true, 'defaultinvites' => 3] + ); + + $response = $this->actingAs($admin)->post(route('admin.user-edit'), [ + 'action' => 'submit', + 'username' => 'new_user', + 'password' => 'password', + 'email' => 'new-user@example.test', + 'role' => (string) $userRole->id, + 'notes' => 'created from test', + ]); + + $response->assertRedirect('admin/user-list'); + $this->assertDatabaseHas('users', [ + 'username' => 'new_user', + 'email' => 'new-user@example.test', + 'roles_id' => $userRole->id, + 'invites' => 3, + 'notes' => 'created from test', + ]); + } + + public function test_admin_create_user_rejects_unknown_role_id(): void + { + $admin = $this->createUserWithRole('Admin', false); + + $response = $this->from('admin/user-edit?action=add') + ->actingAs($admin) + ->post(route('admin.user-edit'), [ + 'action' => 'submit', + 'username' => 'new_user', + 'password' => 'password', + 'email' => 'new-user@example.test', + 'role' => '9999', + 'notes' => '', + ]); + + $response->assertRedirect('admin/user-edit?action=add'); + $response->assertSessionHasErrors('role'); + $this->assertDatabaseMissing('users', ['email' => 'new-user@example.test']); + } + + private function createSchema(): void + { + Schema::create('settings', function (Blueprint $table): void { + $table->string('name')->primary(); + $table->text('value')->nullable(); + }); + + Schema::create('roles', function (Blueprint $table): void { + $table->increments('id'); + $table->string('name'); + $table->string('guard_name'); + $table->integer('rate_limit')->default(60); + $table->boolean('isdefault')->default(false); + $table->unsignedInteger('defaultinvites')->default(0); + $table->timestamps(); + }); + + Schema::create('permissions', function (Blueprint $table): void { + $table->increments('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + }); + + Schema::create('users', function (Blueprint $table): void { + $table->increments('id'); + $table->string('username')->unique(); + $table->string('email')->unique(); + $table->string('password'); + $table->string('host')->default(''); + $table->unsignedInteger('roles_id')->default(1); + $table->unsignedInteger('invites')->default(0); + $table->unsignedInteger('invitedby')->nullable(); + $table->text('notes')->nullable(); + $table->integer('rate_limit')->default(60); + $table->string('api_token')->nullable(); + $table->boolean('verified')->default(true); + $table->boolean('can_post')->default(true); + $table->timestamp('email_verified_at')->nullable(); + $table->timestamp('lastlogin')->nullable(); + $table->rememberToken(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('model_has_roles', function (Blueprint $table): void { + $table->unsignedInteger('role_id'); + $table->string('model_type'); + $table->unsignedInteger('model_id'); + $table->primary(['role_id', 'model_id', 'model_type']); + }); + + Schema::create('model_has_permissions', function (Blueprint $table): void { + $table->unsignedInteger('permission_id'); + $table->string('model_type'); + $table->unsignedInteger('model_id'); + $table->primary(['permission_id', 'model_id', 'model_type']); + }); + + Schema::create('role_has_permissions', function (Blueprint $table): void { + $table->unsignedInteger('permission_id'); + $table->unsignedInteger('role_id'); + $table->primary(['permission_id', 'role_id']); + }); + + Schema::create('user_activities', function (Blueprint $table): void { + $table->increments('id'); + $table->unsignedInteger('user_id')->nullable(); + $table->string('username'); + $table->string('activity_type', 50); + $table->text('description'); + $table->json('metadata')->nullable(); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('root_categories', function (Blueprint $table): void { + $table->increments('id'); + $table->string('title')->default(''); + $table->integer('status')->default(1); + $table->timestamps(); + }); + + Schema::create('categories', function (Blueprint $table): void { + $table->increments('id'); + $table->string('title')->default(''); + $table->unsignedInteger('root_categories_id')->nullable(); + $table->integer('status')->default(1); + $table->timestamps(); + }); + + Schema::create('user_excluded_categories', function (Blueprint $table): void { + $table->increments('id'); + $table->unsignedInteger('users_id'); + $table->unsignedInteger('categories_id'); + }); + } + + private function seedSettings(): void + { + DB::table('settings')->insert([ + ['name' => 'title', 'value' => 'NNTmux Test'], + ['name' => 'home_link', 'value' => '/'], + ['name' => 'categorizeforeign', 'value' => '0'], + ['name' => 'catwebdl', 'value' => '0'], + ]); + + DB::table('root_categories')->insert([ + 'id' => 1, + 'title' => 'General', + 'status' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + private function createUserWithRole(string $roleName, bool $isDefault): User + { + $role = Role::query()->firstOrCreate( + ['name' => $roleName, 'guard_name' => 'web'], + ['rate_limit' => 60, 'isdefault' => $isDefault, 'defaultinvites' => 1] + ); + + $user = User::query()->create([ + 'username' => strtolower($roleName).'_'.Str::random(8), + 'email' => Str::random(12).'@example.test', + 'password' => bcrypt('password'), + 'roles_id' => $role->id, + 'rate_limit' => 60, + 'api_token' => Str::random(32), + 'verified' => true, + 'email_verified_at' => now(), + 'lastlogin' => now(), + ]); + $user->assignRole($role); + + return $user->fresh(); + } +} + diff --git a/tests/Unit/Extensions/HelperCoverUrlTest.php b/tests/Unit/Extensions/HelperCoverUrlTest.php index 20c7b7583..f2a693e8d 100644 --- a/tests/Unit/Extensions/HelperCoverUrlTest.php +++ b/tests/Unit/Extensions/HelperCoverUrlTest.php @@ -8,6 +8,20 @@ use Tests\TestCase; class HelperCoverUrlTest extends TestCase { + public function test_unzip_gzip_file_returns_uncompressed_contents(): void + { + $path = tempnam(sys_get_temp_dir(), 'nntmux-gzip-'); + $this->assertIsString($path); + + file_put_contents($path, gzencode('gzip fixture contents')); + + try { + $this->assertSame('gzip fixture contents', unzipGzipFile($path)); + } finally { + @unlink($path); + } + } + public function test_get_release_cover_returns_placeholder_for_negative_bookinfo_id(): void { $url = getReleaseCover((object) ['bookinfo_id' => -2]);