diff --git a/.env.testing b/.env.testing new file mode 100644 index 000000000..157ae670b --- /dev/null +++ b/.env.testing @@ -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 + diff --git a/app/Models/User.php b/app/Models/User.php index ef26f2d75..9db08c4d9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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; diff --git a/config/database.php b/config/database.php index a49542e2d..825c3c3db 100644 --- a/config/database.php +++ b/config/database.php @@ -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'), diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 000000000..3392f4c8f --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,87 @@ + + */ +class UserFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = User::class; + + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]); + } +} + diff --git a/database/migrations/2024_05_12_192646_add_uuid_column_to_failed_jobs_table.php b/database/migrations/2024_05_12_192646_add_uuid_column_to_failed_jobs_table.php index a437981b7..cd7b78607 100644 --- a/database/migrations/2024_05_12_192646_add_uuid_column_to_failed_jobs_table.php +++ b/database/migrations/2024_05_12_192646_add_uuid_column_to_failed_jobs_table.php @@ -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'); }); diff --git a/phpunit.xml b/phpunit.xml index 888fcef4b..67d9612e9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,6 +13,8 @@ + + diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php index db52b29a5..1af37a84c 100644 --- a/tests/CreatesApplication.php +++ b/tests/CreatesApplication.php @@ -11,6 +11,7 @@ trait CreatesApplication */ public function createApplication() { + // Require the configured application instance. $app = require __DIR__.'/../bootstrap/app.php'; diff --git a/tests/Feature/RoleUpgradeTest.php b/tests/Feature/RoleUpgradeTest.php index 344c82557..fb6e50281 100644 --- a/tests/Feature/RoleUpgradeTest.php +++ b/tests/Feature/RoleUpgradeTest.php @@ -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. diff --git a/tests/Feature/SteamServiceTest.php b/tests/Feature/SteamServiceTest.php index b72693f2a..d7678cb6b 100644 --- a/tests/Feature/SteamServiceTest.php +++ b/tests/Feature/SteamServiceTest.php @@ -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 // ========================================== diff --git a/tests/Support/DatabaseTestCase.php b/tests/Support/DatabaseTestCase.php new file mode 100644 index 000000000..32915b550 --- /dev/null +++ b/tests/Support/DatabaseTestCase.php @@ -0,0 +1,61 @@ + '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; + } +} +