Fix testing on local database

This commit is contained in:
DariusIII
2025-12-14 20:56:43 +01:00
parent 21ccf85d1f
commit a306619824
10 changed files with 373 additions and 10 deletions
+36
View File
@@ -0,0 +1,36 @@
# ============================================
# TESTING ENVIRONMENT - ISOLATED DATABASE
# ============================================
# This file is automatically loaded when running tests.
# Tests use SQLite in-memory database to prevent
# any changes to the real production database.
# ============================================
APP_NAME=NNTmux
APP_ENV=testing
APP_KEY=base64:SomeTestKeyForTestingPurposesOnly123=
APP_DEBUG=true
APP_URL=http://localhost
# CRITICAL: Use SQLite in-memory database for complete isolation
# DO NOT change these values - tests must never touch real database
DB_CONNECTION=testing
DB_DATABASE=:memory:
BCRYPT_ROUNDS=4
CACHE_STORE=array
SESSION_DRIVER=array
QUEUE_CONNECTION=sync
MAIL_MAILER=array
TELESCOPE_ENABLED=false
# Disable external services during testing
NNTP_USERNAME=
NNTP_PASSWORD=
NNTP_SERVER=
NNTP_PORT=
NNTP_SSLENABLED=false
NNTP_SOCKET_TIMEOUT=120
+2
View File
@@ -15,6 +15,7 @@ use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
@@ -105,6 +106,7 @@ use Spatie\Permission\Traits\HasRoles;
*/
final class User extends Authenticatable
{
use HasFactory;
use HasRoles;
use Notifiable;
use SoftDeletes;
+17
View File
@@ -4,7 +4,24 @@ use Illuminate\Support\Str;
return [
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
'testing' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'foreign_key_constraints' => true,
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<User>
*/
protected $model = User::class;
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'username' => fake()->unique()->userName(),
'email' => fake()->unique()->safeEmail(),
'password' => static::$password ??= Hash::make('password'),
'roles_id' => 1,
'api_token' => md5(Str::random(32)),
'grabs' => 0,
'invites' => 0,
'notes' => '',
'movieview' => true,
'xxxview' => false,
'musicview' => true,
'consoleview' => true,
'bookview' => true,
'gameview' => true,
'verified' => true,
'can_post' => true,
'rate_limit' => 60,
'email_verified_at' => now(),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
'verified' => false,
]);
}
/**
* Set a specific role for the user.
*/
public function withRole(int $roleId): static
{
return $this->state(fn (array $attributes) => [
'roles_id' => $roleId,
]);
}
/**
* Set a role expiration date.
*/
public function withRoleExpiration(\DateTimeInterface|string|null $date): static
{
return $this->state(fn (array $attributes) => [
'rolechangedate' => $date,
]);
}
}
@@ -11,6 +11,16 @@ return new class extends Migration
*/
public function up(): void
{
// Skip if table doesn't exist (e.g., fresh SQLite test database)
if (!Schema::hasTable('failed_jobs')) {
return;
}
// Skip if column already exists
if (Schema::hasColumn('failed_jobs', 'uuid')) {
return;
}
Schema::table('failed_jobs', function (Blueprint $table) {
$table->string('uuid')->unique();
});
@@ -21,6 +31,14 @@ return new class extends Migration
*/
public function down(): void
{
if (!Schema::hasTable('failed_jobs')) {
return;
}
if (!Schema::hasColumn('failed_jobs', 'uuid')) {
return;
}
Schema::table('failed_jobs', function (Blueprint $table) {
$table->dropColumn('uuid');
});
+2
View File
@@ -13,6 +13,8 @@
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="testing" force="true"/>
<env name="DB_DATABASE" value=":memory:" force="true"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
+1
View File
@@ -11,6 +11,7 @@ trait CreatesApplication
*/
public function createApplication()
{
// Require the configured application instance.
$app = require __DIR__.'/../bootstrap/app.php';
+130 -7
View File
@@ -2,19 +2,27 @@
namespace Tests\Feature;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
final class RoleUpgradeTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Role $userRole;
private Role $supporterRole;
protected function setUp(): void
{
parent::setUp();
// Use in-memory SQLite database for test isolation
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:']);
DB::purge();
DB::reconnect();
// Create minimal tables needed for testing
$this->createTestTables();
// Create the basic User role
$this->userRole = Role::firstOrCreate(
['name' => 'User'],
@@ -47,19 +55,134 @@ final class RoleUpgradeTest extends TestCase
$this->user = $this->createTestUser($this->userRole->id);
$this->user->assignRole($this->userRole);
}
protected function tearDown(): void
{
DB::disconnect();
parent::tearDown();
}
/**
* Create the minimal database tables needed for testing.
*/
private function createTestTables(): void
{
// Users table
DB::statement('CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
roles_id INTEGER DEFAULT 1,
rolechangedate DATETIME NULL,
pending_roles_id INTEGER NULL,
pending_role_start_date DATETIME NULL,
api_token VARCHAR(255) NULL,
grabs INTEGER DEFAULT 0,
invites INTEGER DEFAULT 0,
notes TEXT DEFAULT "",
movieview INTEGER DEFAULT 1,
xxxview INTEGER DEFAULT 0,
musicview INTEGER DEFAULT 1,
consoleview INTEGER DEFAULT 1,
bookview INTEGER DEFAULT 1,
gameview INTEGER DEFAULT 1,
verified INTEGER DEFAULT 1,
can_post INTEGER DEFAULT 1,
rate_limit INTEGER DEFAULT 60,
email_verified_at DATETIME NULL,
created_at DATETIME NULL,
updated_at DATETIME NULL,
deleted_at DATETIME NULL
)');
// Roles table (spatie/laravel-permission)
DB::statement('CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
guard_name VARCHAR(255) NOT NULL,
addyears INTEGER DEFAULT 0,
apirequests INTEGER DEFAULT 10,
downloadrequests INTEGER DEFAULT 5,
defaultinvites INTEGER DEFAULT 0,
isdefault INTEGER DEFAULT 0,
donation INTEGER DEFAULT 0,
canpreview INTEGER DEFAULT 0,
created_at DATETIME NULL,
updated_at DATETIME NULL
)');
// Model has roles table (spatie/laravel-permission)
DB::statement('CREATE TABLE IF NOT EXISTS model_has_roles (
role_id INTEGER NOT NULL,
model_type VARCHAR(255) NOT NULL,
model_id INTEGER NOT NULL,
PRIMARY KEY (role_id, model_type, model_id)
)');
// Permissions table (spatie/laravel-permission)
DB::statement('CREATE TABLE IF NOT EXISTS permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
guard_name VARCHAR(255) NOT NULL,
created_at DATETIME NULL,
updated_at DATETIME NULL
)');
// Model has permissions table (spatie/laravel-permission)
DB::statement('CREATE TABLE IF NOT EXISTS model_has_permissions (
permission_id INTEGER NOT NULL,
model_type VARCHAR(255) NOT NULL,
model_id INTEGER NOT NULL,
PRIMARY KEY (permission_id, model_type, model_id)
)');
// Role has permissions table (spatie/laravel-permission)
DB::statement('CREATE TABLE IF NOT EXISTS role_has_permissions (
permission_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
PRIMARY KEY (permission_id, role_id)
)');
// User role history table (if needed)
DB::statement('CREATE TABLE IF NOT EXISTS user_role_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
users_id INTEGER NOT NULL,
old_roles_id INTEGER NULL,
new_roles_id INTEGER NOT NULL,
old_rolechangedate DATETIME NULL,
new_rolechangedate DATETIME NULL,
changed_by_user_id INTEGER NULL,
reason TEXT NULL,
created_at DATETIME NULL
)');
}
/**
* Helper to create a test user without factory.
*/
private function createTestUser(int $roleId, ?string $roleChangeDate = null): User
{
return User::create([
'username' => 'testuser_'.Str::random(8),
'email' => Str::random(8).'@test.com',
'password' => bcrypt('password'),
// Use forceCreate to bypass hashed cast
$user = new User();
$user->username = 'testuser_'.Str::random(8);
$user->email = Str::random(8).'@test.com';
$user->roles_id = $roleId;
$user->rolechangedate = $roleChangeDate;
$user->api_token = md5(Str::random(32));
// Insert directly to avoid hashed cast issue
DB::table('users')->insert([
'username' => $user->username,
'email' => $user->email,
'password' => 'hashed_password_placeholder',
'roles_id' => $roleId,
'rolechangedate' => $roleChangeDate,
'api_token' => md5(Str::random(32)),
'api_token' => $user->api_token,
'created_at' => now(),
'updated_at' => now(),
]);
return User::where('email', $user->email)->first();
}
/**
* Test that updating a user role to Supporter with addYears=2 correctly applies 2 years.
+19 -3
View File
@@ -6,8 +6,8 @@ namespace Tests\Feature;
use App\Models\SteamApp;
use App\Services\SteamService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
@@ -16,17 +16,33 @@ use Tests\TestCase;
*/
class SteamServiceTest extends TestCase
{
use RefreshDatabase;
protected SteamService $service;
protected function setUp(): void
{
parent::setUp();
// Use in-memory SQLite database for test isolation
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:']);
DB::purge();
DB::reconnect();
// Create minimal steam_apps table for testing
DB::statement('CREATE TABLE IF NOT EXISTS steam_apps (
appid INTEGER PRIMARY KEY,
name VARCHAR(255)
)');
$this->service = new SteamService();
Cache::flush();
}
protected function tearDown(): void
{
DB::disconnect();
parent::tearDown();
}
// ==========================================
// Search Tests with Mocked Database
// ==========================================
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace Tests\Support;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema;
/**
* Trait DatabaseTestCase
*
* A trait for tests that need database access but should use an isolated
* SQLite in-memory database instead of the real database.
*
* This trait:
* - Ensures the test uses an in-memory SQLite database
* - Runs migrations for clean test state
* - Prevents tests from affecting production data
*
* Usage:
* use Tests\Support\DatabaseTestCase;
*
* class MyTest extends TestCase
* {
* use DatabaseTestCase;
* // ...
* }
*/
trait DatabaseTestCase
{
use RefreshDatabase;
/**
* Define hooks to migrate the database before and after each test.
*
* @return void
*/
protected function setUpDatabaseTestCase(): void
{
// Ensure we're using the testing connection (SQLite in-memory)
if (config('database.default') !== 'testing') {
config(['database.default' => 'testing']);
}
}
/**
* Boot the testing trait for this class.
*
* @return void
*/
protected function setUpTraits(): array
{
$uses = parent::setUpTraits();
if (isset($uses[DatabaseTestCase::class])) {
$this->setUpDatabaseTestCase();
}
return $uses;
}
}